当前位置: 首页 > news >正文

【人人都能读标准】11. 原理篇总结:一个程序的完整执行过程

本文为《人人都能读标准》—— ECMAScript篇的第11篇。我在这个仓库中系统地介绍了标准的阅读规则以及使用方式,并深入剖析了标准对JavaScript核心原理的描述。


我们一路走了很远很远,终于到了本书原理篇的最后一站。

在原理篇中,我们先讲了语言的文法模型、阅读规则以及基于这个文法模型构建的解析树;然后我们扩展到标准使用的两类算法,分别是抽象操作以及用于表达解析树语义的语法导向操作;随后,我们马不停蹄地讲到标准用来表示算法中间结果以及语言抽象概念的规范类型;最后,我们使用3个章节从大到小讲了ECMAScript的执行环境,分别是agents、调用栈、执行上下文、Realm、作用域与作用域链。

有了这些基础,我们就有了理解ECMAScript程序整个执行过程的所有“拼图”,本节是对原理篇讲的所有内容的一个梳理与串联,我会先概括性地讲ECMAScript程序执行的一般过程,然后我会使用一段著名的代码片段作为案例,为你展示ECMAScript程序实际的执行过程。


一个程序的执行过程

一个典型的ECMAScript程序,执行时会经历以下三个阶段:

  1. 初始化Realm环境:InitializeHostDefinedRealm()

  2. 解析脚本:ParseScript()

  3. 执行脚本:ScriptEvaluation()


初始化Realm环境是所有程序执行前的必经阶段,Realm会给程序提供最少必要的运行资源。在这个阶段中,会创建一个全局执行上下文以及一个Realm记录器。Realm记录器包含了由ECMAScript标准定义的所有固有对象、一个很大一部分由宿主定义的全局对象、以及一个全局环境记录器。

初始化完毕的Realm,可以在未来给多个脚本使用,比如一个HTML页面中不同的Script标签都会使用同一个Realm。

而每一段脚本执行前,需要先解析脚本,脚本解析主要经历以下两个过程:

  1. 语法解析:基于词法文法对代码进行词法分析,得到代码中所有的输入元素;以目标符Script对这些输入元素进行句法分析,并最终得到一颗解析树;
  2. 对解析树所有的节点进行先验错误的检查:
    1. 如果有错误,返回一个列表,这个列表包含一个或以上的SyntaxError对象,表示所有提前发现的错误,并直接终止程序的执行;
    2. 如果没有错误,则返回一个脚本记录器,记录上一步创建的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()的执行频率。

下图是这段程序的执行过程的总结:

execution-summary

这段代码的执行会经历我们前面讲的程序执行的三个过程:初始化Realm环境、解析脚本、执行脚本。在执行脚本的过程中,最终会通过debounce(handleScroll)创建函数closure,用作scroll事件的绑定逻辑。等到用户触发scroll事件的时候,这段防抖程序早已执行完毕,但宿主环境会帮助我们触发closure的逻辑。

在这张图中,插了红旗的地方是我在下面重点关注的步骤:


1. 初始化Realm环境

这段防抖代码显然是在浏览器宿主中执行,浏览器宿主所创建的全局对象是我们非常熟悉的window 对象。因此,在这一步中,得到的Realm记录器重要的字段如下:

{[[Intrinsics]]: {...固有对象}[[GlobalObject]]: window[[GlobalEnv]]: 一个全局环境记录器
}

全局环境记录器记录了全局对象的属性方法,我们在这段代码中使用的windowclearTimeoutsetTimeout这些标识符,在此时已经绑定在全局环境记录器的[[ObjectRecord]]字段当中,静候我们的调用。

完成初始化后,调用栈如下图所示:

stack1


2. 解析脚本

通过语法解析后,上面的代码得到这样一颗树,不必担心这颗树看起来有点复杂,我们后续都会进行逐一拆解的。

parse-tree

你也可以使用我在5.文法汇总提到的方法,利用js解析器acorn自行解析得到这颗树,并使用JSON可视化工具来可视化这颗树。

这段代码通过了所有先验错误的检查,最终我们得到脚本记录器如下:

{[[Realm]]: 第一步得到的Realm记录器[[ECMAScriptCode]]: 解析树 
}

3. 执行脚本

执行脚本的过程会由ScriptEvaluation()触发,我们在9.作用域其实已经拆解过这个抽象操作了:

script-evaluation

它会先(2)创建全局执行上下文,(3~8)初始化执行上下文中的组件,(10)把执行上下文压入调用栈,接着进行(12)全局声明实例化。

我们可以从解析树的片段中看到,全局代码有三个语句,分别是2个函数声明语句以及1个表达式语句。

parse-tree1

在进行全局声明实例化的时候,函数声明语句会被识别为变量声明,因而函数对象会被创建,函数标识符会被绑定在全局环境记录器中,并初始化值为函数对象。在这颗解析树上,我们看到两个函数声明语句的标识符分别是debouncehandleScroll,因而,当完成全局声明实例化时,调用栈的样子如下图所示:

stack2

此后,开始(13.a)执行Script求值语义。我们在6.算法提到过,基于链式产生式的特点,对Script的求值最终会导向对其“后代节点”语句列表StatementList的求值。在7.规范类型中,我又进一步给你展示了语句列表求值语义的详细过程,总的来说就是依次执行语句列表中的语句,直到执行完毕或被“硬性完成”提前终止。在这段防抖代码中,对语句列表的求值便是依次执行两个函数声明语句以及1个表达式语句

函数声明语句的求值语义会直接返回一个空值,不会产生任何实际效果。这是因为函数声明语句在全局声明实例化的时候已经发挥作用了。

而相对复杂的是后面的表达式语句。从下面的解析树片段你可以看到,这个表达式语句包含的是一个函数调用表达式CallExpression,并可以进一步分解成一个成员表达式MemberExpression以及一个参数表达式Arguments。

parse-tree2

从CallExpression的求值语义我们可以看到,它主要做这么两个事情:

  1. (下图红色标号1)它会先对MemberExpression求值,获取对应的函数,这里得到的是一个宿主的内置函数window.addEventListener
  2. (标号2)然后通过抽象操作EvaluateCall执行函数,此时会把参数也传入这个抽象操作中使用。

CallExpression

在抽象操作EvaluateCall实际执行window.addEventListener前,它会先对参数进行求值,获得参数的值(下图框出部分)。在我们防抖代码的例子里,此时便是开始执行debounce(handleScroll)的时候。

EvaluateCall


执行debounce(handleScroll)

函数执行的核心逻辑来自于它的[[call]]方法,这个方法主要完成以下这么几个事情:

  1. 创建并初始化执行上下文;
  2. 函数声明实例化;
  3. 执行函数语句列表;
  4. 弹出执行上下文

当然,这只是一个大致算法轮廓,并不完整。在应用篇14.函数中,我会对函数的执行过程有更加详细的介绍。

函数声明实例化的过程我在9.作用域做了非常详细的介绍,它不仅会像全局环境一样绑定4种典型的声明语句的标识符,还会初始化参数,并按需创建一个arguments对象。当完成debounce函数声明实例化的时候,环境中的调用栈如下图所示:

stack3

在创建closure函数的过程中,会将函数对象的[[Environment]]内部插槽设置为debounce函数环境记录器,用于后续构建closure函数的作用域链。

此后,我们开始依次执行函数的语句列表内的语句。

parse-tree3

从上面的解析树片段我们可以看到,该函数有三个语句(绿色部分):

  1. 词法声明语句。用于初始化词法标识符timer,执行完这个语句之后,timer才可以被其他代码使用。
  2. 函数声明语句,在函数创建阶段已经发挥作用了,此时直接跳过。
  3. return语句,返回closure函数对象,结束函数的执行。

完成debounce函数的执行后,debounce执行上下文会弹出调用栈,并被销毁。

随后,就是内置函数window.addEventListener的执行,他会给window添加一个scroll事件的监听,监听的逻辑就是执行debounce返回的闭包函数closure。


4. scroll事件触发

当scroll事件触发的时候,宿主会自动触发闭包函数closure的逻辑。而执行一个函数实际上还是经过以上四个步骤:

  1. 创建并初始化执行上下文;

  2. 函数声明实例化。在函数声明实例化之前,closure函数会先创建函数环境记录器,并把[[OuterEnv]]指向自己[[Environment]]内部插槽中保存的环境记录器(即debounce函数环境记录器),从而使得closure函数可以访问已经执行完毕的debounce函数内部的变量。当closure函数第一次被触发时,调用栈如下所示(此时timer仍为null):

    stack4

  3. 执行函数语句列表;closure有两个表达式语句。

    • 第一个函数调用表达式语句会执行内置函数clearTimeout(),用以重置定时器timer。

    • 第二个赋值表达式会创建新的定时器并把定时器的序号赋值在变量timer上,而定时器设定的逻辑,就是触发我们的handleScroll()

      parse-tree4

  4. 弹出执行上下文。

在此之后,如果在定时器设定的时间内没有再触发过这个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() 函数返回文件对象&#xff…...

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…...

AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南

点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...

椭圆曲线密码学(ECC)

一、ECC算法概述 椭圆曲线密码学(Elliptic Curve Cryptography)是基于椭圆曲线数学理论的公钥密码系统,由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA,ECC在相同安全强度下密钥更短(256位ECC ≈ 3072位RSA…...

React hook之useRef

React useRef 详解 useRef 是 React 提供的一个 Hook,用于在函数组件中创建可变的引用对象。它在 React 开发中有多种重要用途,下面我将全面详细地介绍它的特性和用法。 基本概念 1. 创建 ref const refContainer useRef(initialValue);initialValu…...

Oracle查询表空间大小

1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...

Java多线程实现之Callable接口深度解析

Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...

Redis数据倾斜问题解决

Redis 数据倾斜问题解析与解决方案 什么是 Redis 数据倾斜 Redis 数据倾斜指的是在 Redis 集群中,部分节点存储的数据量或访问量远高于其他节点,导致这些节点负载过高,影响整体性能。 数据倾斜的主要表现 部分节点内存使用率远高于其他节…...

鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南

1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发,使用DevEco Studio作为开发工具,采用Java语言实现,包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...

Java毕业设计:WML信息查询与后端信息发布系统开发

JAVAWML信息查询与后端信息发布系统实现 一、系统概述 本系统基于Java和WML(无线标记语言)技术开发,实现了移动设备上的信息查询与后端信息发布功能。系统采用B/S架构,服务器端使用Java Servlet处理请求,数据库采用MySQL存储信息&#xff0…...

4. TypeScript 类型推断与类型组合

一、类型推断 (一) 什么是类型推断 TypeScript 的类型推断会根据变量、函数返回值、对象和数组的赋值和使用方式,自动确定它们的类型。 这一特性减少了显式类型注解的需要,在保持类型安全的同时简化了代码。通过分析上下文和初始值,TypeSc…...

数据结构:递归的种类(Types of Recursion)

目录 尾递归(Tail Recursion) 什么是 Loop(循环)? 复杂度分析 头递归(Head Recursion) 树形递归(Tree Recursion) 线性递归(Linear Recursion)…...