网站建设属什么资产,怎样建设自己的商业网站,哈尔滨工程交易信息网,pc端手机网站 样式没居中本文为《人人都能读标准》—— ECMAScript篇的第11篇。我在这个仓库中系统地介绍了标准的阅读规则以及使用方式#xff0c;并深入剖析了标准对JavaScript核心原理的描述。 我们一路走了很远很远#xff0c;终于到了本书原理篇的最后一站。
在原理篇中#xff0c;我们先讲了… 本文为《人人都能读标准》—— 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创建全局执行上下文38初始化执行上下文中的组件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()的逻辑就会被触发从而实现了防抖的效果。