【人人都能读标准】11. 原理篇总结:一个程序的完整执行过程
本文为《人人都能读标准》—— ECMAScript篇的第11篇。我在这个仓库中系统地介绍了标准的阅读规则以及使用方式,并深入剖析了标准对JavaScript核心原理的描述。
我们一路走了很远很远,终于到了本书原理篇的最后一站。
在原理篇中,我们先讲了语言的文法模型、阅读规则以及基于这个文法模型构建的解析树;然后我们扩展到标准使用的两类算法,分别是抽象操作以及用于表达解析树语义的语法导向操作;随后,我们马不停蹄地讲到标准用来表示算法中间结果以及语言抽象概念的规范类型;最后,我们使用3个章节从大到小讲了ECMAScript的执行环境,分别是agents、调用栈、执行上下文、Realm、作用域与作用域链。
有了这些基础,我们就有了理解ECMAScript程序整个执行过程的所有“拼图”,本节是对原理篇讲的所有内容的一个梳理与串联,我会先概括性地讲ECMAScript程序执行的一般过程,然后我会使用一段著名的代码片段作为案例,为你展示ECMAScript程序实际的执行过程。
一个程序的执行过程
一个典型的ECMAScript程序,执行时会经历以下三个阶段:
-
初始化Realm环境:InitializeHostDefinedRealm()
-
解析脚本:ParseScript()
-
执行脚本:ScriptEvaluation()
初始化Realm环境是所有程序执行前的必经阶段,Realm会给程序提供最少必要的运行资源。在这个阶段中,会创建一个全局执行上下文以及一个Realm记录器。Realm记录器包含了由ECMAScript标准定义的所有固有对象、一个很大一部分由宿主定义的全局对象、以及一个全局环境记录器。
初始化完毕的Realm,可以在未来给多个脚本使用,比如一个HTML页面中不同的Script标签都会使用同一个Realm。
而每一段脚本执行前,需要先解析脚本,脚本解析主要经历以下两个过程:
- 语法解析:基于词法文法对代码进行词法分析,得到代码中所有的输入元素;以目标符Script对这些输入元素进行句法分析,并最终得到一颗解析树;
- 对解析树所有的节点进行先验错误的检查:
- 如果有错误,返回一个列表,这个列表包含一个或以上的SyntaxError对象,表示所有提前发现的错误,并直接终止程序的执行;
- 如果没有错误,则返回一个脚本记录器,记录上一步创建的Realm记录器以及这一步得到的解析树等信息。
在执行脚本的阶段,会先进行全局声明实例化,把全局标识符绑定到全局环境记录器中。接着会调用解析树根节点Script的求值语义,并最终完成整个程序的执行。
以一段著名的程序为例
防抖/节流是特别火的面试题,它们都用于限制事件的频繁触发。防抖的作用是:当事件在短时间内多次发生时,只触发最后一个事件的逻辑。
一段实现防抖的代码如下:
function debounce(func, delay) {let timer = null;function closure(){clearTimeout(timer);timer = setTimeout(() => func(...arguments), delay || 250)}return closure
}
function handleScroll() {console.log("heavy work")
}
window.addEventListener('scroll', debounce(handleScroll))
在这里,debounce()
是防抖函数,handleScroll()
是我打算让scroll事件监听的逻辑。通过调用debounce(handleScroll)
会返回一个闭包函数closure
,该闭包通过”私有变量”timer控制handleScroll()
的执行频率。
下图是这段程序的执行过程的总结:
这段代码的执行会经历我们前面讲的程序执行的三个过程:初始化Realm环境、解析脚本、执行脚本。在执行脚本的过程中,最终会通过debounce(handleScroll)
创建函数closure,用作scroll事件的绑定逻辑。等到用户触发scroll事件的时候,这段防抖程序早已执行完毕,但宿主环境会帮助我们触发closure的逻辑。
在这张图中,插了红旗的地方是我在下面重点关注的步骤:
1. 初始化Realm环境
这段防抖代码显然是在浏览器宿主中执行,浏览器宿主所创建的全局对象是我们非常熟悉的window
对象。因此,在这一步中,得到的Realm记录器重要的字段如下:
{[[Intrinsics]]: {...固有对象}[[GlobalObject]]: window[[GlobalEnv]]: 一个全局环境记录器
}
全局环境记录器记录了全局对象的属性方法,我们在这段代码中使用的window
、clearTimeout
、setTimeout
这些标识符,在此时已经绑定在全局环境记录器的[[ObjectRecord]]
字段当中,静候我们的调用。
完成初始化后,调用栈如下图所示:
2. 解析脚本
通过语法解析后,上面的代码得到这样一颗树,不必担心这颗树看起来有点复杂,我们后续都会进行逐一拆解的。
你也可以使用我在5.文法汇总提到的方法,利用js解析器acorn自行解析得到这颗树,并使用JSON可视化工具来可视化这颗树。
这段代码通过了所有先验错误的检查,最终我们得到脚本记录器如下:
{[[Realm]]: 第一步得到的Realm记录器[[ECMAScriptCode]]: 解析树
}
3. 执行脚本
执行脚本的过程会由ScriptEvaluation()触发,我们在9.作用域其实已经拆解过这个抽象操作了:
它会先(2)创建全局执行上下文,(3~8)初始化执行上下文中的组件,(10)把执行上下文压入调用栈,接着进行(12)全局声明实例化。
我们可以从解析树的片段中看到,全局代码有三个语句,分别是2个函数声明语句以及1个表达式语句。
在进行全局声明实例化的时候,函数声明语句会被识别为变量声明,因而函数对象会被创建,函数标识符会被绑定在全局环境记录器中,并初始化值为函数对象。在这颗解析树上,我们看到两个函数声明语句的标识符分别是debounce
、handleScroll
,因而,当完成全局声明实例化时,调用栈的样子如下图所示:
此后,开始(13.a)执行Script求值语义。我们在6.算法提到过,基于链式产生式的特点,对Script的求值最终会导向对其“后代节点”语句列表StatementList的求值。在7.规范类型中,我又进一步给你展示了语句列表求值语义的详细过程,总的来说就是依次执行语句列表中的语句,直到执行完毕或被“硬性完成”提前终止。在这段防抖代码中,对语句列表的求值便是依次执行两个函数声明语句以及1个表达式语句
函数声明语句的求值语义会直接返回一个空值,不会产生任何实际效果。这是因为函数声明语句在全局声明实例化的时候已经发挥作用了。
而相对复杂的是后面的表达式语句。从下面的解析树片段你可以看到,这个表达式语句包含的是一个函数调用表达式CallExpression,并可以进一步分解成一个成员表达式MemberExpression以及一个参数表达式Arguments。
从CallExpression的求值语义我们可以看到,它主要做这么两个事情:
- (下图红色标号1)它会先对MemberExpression求值,获取对应的函数,这里得到的是一个宿主的内置函数
window.addEventListener
; - (标号2)然后通过抽象操作EvaluateCall执行函数,此时会把参数也传入这个抽象操作中使用。
在抽象操作EvaluateCall实际执行window.addEventListener
前,它会先对参数进行求值,获得参数的值(下图框出部分)。在我们防抖代码的例子里,此时便是开始执行debounce(handleScroll)
的时候。
执行debounce(handleScroll)
函数执行的核心逻辑来自于它的[[call]]方法,这个方法主要完成以下这么几个事情:
- 创建并初始化执行上下文;
- 函数声明实例化;
- 执行函数语句列表;
- 弹出执行上下文
当然,这只是一个大致算法轮廓,并不完整。在应用篇14.函数中,我会对函数的执行过程有更加详细的介绍。
函数声明实例化的过程我在9.作用域做了非常详细的介绍,它不仅会像全局环境一样绑定4种典型的声明语句的标识符,还会初始化参数,并按需创建一个arguments对象。当完成debounce函数声明实例化的时候,环境中的调用栈如下图所示:
在创建closure函数的过程中,会将函数对象的[[Environment]]
内部插槽设置为debounce函数环境记录器,用于后续构建closure函数的作用域链。
此后,我们开始依次执行函数的语句列表内的语句。
从上面的解析树片段我们可以看到,该函数有三个语句(绿色部分):
- 词法声明语句。用于初始化词法标识符timer,执行完这个语句之后,timer才可以被其他代码使用。
- 函数声明语句,在函数创建阶段已经发挥作用了,此时直接跳过。
- return语句,返回closure函数对象,结束函数的执行。
完成debounce函数的执行后,debounce执行上下文会弹出调用栈,并被销毁。
随后,就是内置函数window.addEventListener
的执行,他会给window添加一个scroll事件的监听,监听的逻辑就是执行debounce返回的闭包函数closure。
4. scroll事件触发
当scroll事件触发的时候,宿主会自动触发闭包函数closure的逻辑。而执行一个函数实际上还是经过以上四个步骤:
-
创建并初始化执行上下文;
-
函数声明实例化。在函数声明实例化之前,closure函数会先创建函数环境记录器,并把
[[OuterEnv]]
指向自己[[Environment]]
内部插槽中保存的环境记录器(即debounce函数环境记录器),从而使得closure函数可以访问已经执行完毕的debounce函数内部的变量。当closure函数第一次被触发时,调用栈如下所示(此时timer仍为null): -
执行函数语句列表;closure有两个表达式语句。
-
第一个函数调用表达式语句会执行内置函数
clearTimeout()
,用以重置定时器timer。 -
第二个赋值表达式会创建新的定时器并把定时器的序号赋值在变量timer上,而定时器设定的逻辑,就是触发我们的
handleScroll()
。
-
-
弹出执行上下文。
在此之后,如果在定时器设定的时间内没有再触发过这个closure函数,那么handleScroll()
的逻辑就会被触发,从而实现了防抖的效果。
相关文章:

【人人都能读标准】11. 原理篇总结:一个程序的完整执行过程
本文为《人人都能读标准》—— ECMAScript篇的第11篇。我在这个仓库中系统地介绍了标准的阅读规则以及使用方式,并深入剖析了标准对JavaScript核心原理的描述。 我们一路走了很远很远,终于到了本书原理篇的最后一站。 在原理篇中,我们先讲了…...

sheng的学习笔记-IO多路复用,NIO,BIO,AIO
基础概念IO分为几种:同步阻塞的BIO,同步非阻塞的NIO,异步非阻塞AIO,IO多路复用,信号驱动IO(不常用)对于一个network IO,它会涉及到两个系统对象,一个是调用这个IO的proce…...

【Python入门第三十五天】Python丨文件打开
在服务器上打开文件 假设我们有以下文件,位于与 Python 相同的文件夹中。 demofile.txt Hello! Welcome to demofile.txt This file is for testing purposes. Good Luck!如需打开文件,请使用内建的 open() 函数。 open() 函数返回文件对象ÿ…...

jsoup 框架的使用指南
概述 参考: 官方文档jsoup的使用JSoup教程jsoup 在 GitHub 的开源代码 概念简介 jsoup 是一款基于 Java 的 HTML 解析器,它提供了一套非常省力的 API,不但能直接解析某个 URL 地址、HTML 文本内容,而且还能通过类似于 DOM、CS…...

web前端开发和后端开发哪个难度大?
前言 因为涉及到的具体的应用的领域不同,所以说不能简单地说哪一个难,对于前端而言你会感觉到入门会非常的简单,这也是会给许多人一种错觉,前端很简单,但是只能说是在入门理解上是有利于新手的,前端在主要…...

认证与认可之间有什么区别和联系?
认证与认可之间有什么区别和联系? 当今社会,认证与认可已经深入企业的生活,那么认证与认可之间到底有什么区别和联系呢? 认证,是指由认证机构证明产品、服务、管理体系符合相关技术规范、相关技术规范的强制性要求或者…...

【Java|golang】1626. 无矛盾的最佳球队---最长子序列,不连续,二维数组排序
假设你是球队的经理。对于即将到来的锦标赛,你想组合一支总体得分最高的球队。球队的得分是球队中所有球员的分数 总和 。 然而,球队中的矛盾会限制球员的发挥,所以必须选出一支 没有矛盾 的球队。如果一名年龄较小球员的分数 严格大于 一名…...

C++ 八股文(简单面试题)
1.左值 可寻址变量,持久性; 2.右值 没有变量名,不可寻址,短暂性; 3.指针 指向的内存地址,指针变量存储的就是指向的对象的首地址 4.引用 为一个变量起别名,定义引用的时候一定要初始化&a…...

RK3588平台开发系列讲解(显示篇)DP显示调试方法
平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、查看 connector 状态二、强制使能/禁⽤ DP三、DPCP 读写四、Type-C 接口 Debug五、查看 DP 寄存器六、查看 VOP 状态七、查看当前显示时钟八、调整 DRM log 等级沉淀、分享、成长,让自己和他人都能有所收获!😄…...

模拟请求发生跨域问题
参考:传送门 问题产生: Access to XMLHttpRequest at ‘http://test-cms.jinhuahuolong.com/api/pages/list’ from origin ‘null’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resourc…...

Qt实践项目:仿Everything软件实现一个QtEverything
⭐️我叫忆_恒心,一名喜欢书写博客的在读研究生👨🎓。 如果觉得本文能帮到您,麻烦点个赞👍呗! 近期会不断在专栏里进行更新讲解博客~~~ 有什么问题的小伙伴 欢迎留言提问欧,喜欢的小伙伴给个三…...

WEB网站服务(一)
1.1 Apache网站服务基础1.1.1Apache简介Apache HTTP Server是开源软件项目的杰出代表,基于标准的HTTP网络协议提供网页浏览服务。Apache服务器可以运行在Linux,UNIX,windows等多种操作系统平台中。1.Apache的起源1995年,Apache服务程序的1.0版…...

Python数据分析script必备知识(一)
Python数据分析script必备知识(一) 1.重定向终端输出内容 使生成的结果移动到其他位置 # 重定向, 使生成的结果移动到其他位置 import syssys.stderr = sys.stdoutprint(dir(sys)) # ,,,,,__stderr__, __stdin__, __stdout__,,,,,,# 使用场景:脚本上线时,想要把输出结果…...

初识linux之管道
一、进程间通信的概念大家都知道,进程是具有独立性的,因为一个程序运行起来生成进程时,也会生成它的进程结构体,即PCB,然后然后通过进程结构体中的结构体指针找到它的虚拟地址空间,然后再通过它的页表映射到…...

C++成神之路 | 第一课【步入C++的世界】
目录 一、认识C++ 1.1、关于 C++ 1.2、C++的前世今生 1.2.1、C+...

【面试题】大厂面试官:你做过什么有亮点的项目吗?
大厂面试题分享 面试题库前后端面试题库 (面试必备) 推荐:★★★★★地址:前端面试题库前言大厂面试中除了问常见的算法网络基础,和一些八股文手写体之外,经常出现的一个问题就是,你做过什么项目…...

Springboot Long类型数据太长返回给前端,精度丢失问题 复现、解决
前言 惯例,收到兄弟求救,关于long类型丢失精度的问题: 存在一个初学者不会,就会有第二个初学者不会,所以我出手。 正文 不多说,开搞。 如题, 后端返回的数据 给到 前端, Long类型数…...

Anaconda虚拟环境的创建方法(命令创建)
虚拟环境介绍: 虚拟环境是一为某个项目创建的专属于它的python包,因此做python项目时,一般一个项目用一个虚拟环境。在实际开发中,如果项目A需要某个包的1.0版本,项目B需要此包的2.0版本。如果没有安装虚拟环境&#…...

数据结构——树与二叉树
作者:几冬雪来 时间:2023年3月22日 内容:数据结构树与二叉树的讲解(介绍) 目录 前言: 1.树的概念: 2.树与非树: 3.树的定义: 4.树的应用: 二叉树&…...

vue后台管理系统
后面可参考下:vue系列(三)——手把手教你搭建一个vue3管理后台基础模板 以下代码项目gitee地址 文章目录1. 初始化前端项目初始化项目添加加载效果配置 vite.config.js2. 使用路由安装路由配置路由配置别名和跳转安装pathvite.config.jsjsco…...

spring boot 集成 postgis jar
要将 PostGIS 集成到 Spring Boot 应用程序中,需要按照以下步骤进行操作:1. 将 PostGIS JDBC 驱动程序添加到项目依赖项中。可以在 Maven 或 Gradle 中添加以下依赖项:Maven:```xml <dependency><groupId>org.postgresql</groupId><artifactId>pos…...

【Java进阶篇】——反射机制
一、反射的概念 1.1 反射出现的背景 Java程序中,所有对象都有两种类型:编译时类型和运行时类型,而很多时候对象的编译时类型和运行时类型不一致 Object obj new String("hello")、obj.getClass(); 如果某些变量或形参的声明类型…...

Oracle中含有recover 状态的数据文件环境中,做异机恢复
背景: 我们在一些恢复测试案例中,会经常遇到一些奇怪的问题,其中有的是源端数据文件不规范而导致恢复过程出错,比较常见的错误有: 数据文件名称重复(如:/oradata1/user01.dbf 和 /oradata2/us…...

图像识别模型
一、数据准备 首先要做一些数据准备方面的工作:一是把数据集切分为训练集和验证集, 二是转换为tfrecord 格式。在data_prepare/文件夹中提供了会用到的数据集和代码。首先要将自己的数据集切分为训练集和验证集,训练集用于训练模型…...

[零刻]EQ12 N100 迷你主机:从开箱到安装ESXi+虚拟机
开箱先上图:配置详情:EQ12采用了Intel最新推出的N100系列的处理,超低的功耗,以及出色的CPU性能用来做软路由或者是All in one 相当不错,CPU带有主动散热风扇,在长期运行下散热完全不用担心,性价…...

MongoDB基础
优质博客 IT-BLOG-CN 一、简介 MongoDB是一个强大的分布式文件存储的NoSQL数据库,天然支持高可用、分布式和灵活设计。由C编写,运行稳定,性能高。为WEB应用提供可扩展的高性能数据存储解决方案。主要解决关系型数据库数据量大,并…...

【Linux】Linux基本指令(下)
前言: 紧接上期【Linux】基本指令(上)的学习,今天我们继续学习基本指令操作,深入探讨指令的基本知识。 目录 (一)常用指令 👉more指令 👉less指令(重要&…...

基于uniapp+u-view开发小程序【技术点整理】
一、上传图片 1.实现效果: 2.具体代码: <template><view><view class"imgbox"><view>职业证书</view><!-- 上传图片 --><u-upload :fileList"fileList1" afterRead"afterRead"…...

投稿指南【NO.7】目标检测论文写作模板(初稿)
中文标题(名词性短语,少于20字,尽量不使用外文缩写词)张晓敏1,作者1,2***,作者2**,作者2*(通信作者右上标*)1中国科学院上海光学精密机械研究所空间激光传输与探测技术重…...

【绘图】比Matplotlib更强大:ProPlot
✅作者简介:在读博士,伪程序媛,人工智能领域学习者,深耕机器学习,交叉学科实践者,周更前沿文章解读,提供科研小工具,分享科研经验,欢迎交流!📌个人…...