JS 执行流程
渲染引擎在解析
HTML
过程中遇到script
标签时,会暂停解析并开始请求脚本(需要script
标签没有defer
或async
属性),脚本内容响应后作为一个宏任务放入消息队列(Message Queue
)中事件循环(
Event Loop
)机制从消息队列取出脚本任务后,引擎对代码进行词法分析,将代码拆分为一个个词法单元(Token
)引擎对代码进行语法分析,根据词法单元进行语法检查(例如操作数能否应用到运算符)并组成抽象语法树(
AST
),这个过程还会将变量和函数声明等添加到符号表(会记录名称、类型、位置、作用域等属性)尝试执行代码,解释器首先会创建全局执行上下文并放入调用栈
创建执行上下文的同时会再次遍历抽象语法树并进行词法分析:
- 生成字节码并作为执行上下文的可执行代码
- 创建变量环境,
var
声明的变量会进行提升并以undefined
作为默认值存储在变量环境(执行上下文对象就是所谓的栈空间);函数也会进行提升,函数对象会保存在堆空间中,然后以堆中的地址作为值存储在变量环境中。变量环境中的值在赋值前就可以访问。 - 创建词法环境,词法环境也是一个栈结构,
let
、const
创建的变量会存储在词法环境栈,初始为undefined
,但如果在未赋值前访问会报错(即使外部环境有同名变量)。 - 分析执行上下文中
this
的值并存储 - 创建对外部环境的引用,形成作用域链
解释器逐条解释执行全局执行上下文中的可执行代码,并使用即时编译(
JIT
)和缓存等方式进行优化(将常用代码编译为机器码、缓存已执行代码、使用计算结果替换计算指令等)当创建一个对象时,如果对象体积较大会保存在堆空间的老生代区域,否则保存在新生代区域,变量环境和词法环境中的变量保存堆中的地址
当遇到块级作用域时,会创建一个新的环境对象并放入词法环境栈。与词法环境一样,变量初始值为
undefined
,但在未赋值时无法访问;在代码对变量进行访问时会从词法环境的栈顶到栈底依次查找,之后再查找变量环境和作用域链上的外部环境当执行函数时,创建函数执行上下文,并放入调用栈(同第 5 步),函数执行结束后出栈
出栈实际是标记当前执行栈的指针下移,在有新的执行上下文入栈时覆盖需要出栈的上下文。在上下文被覆盖时,存储在栈中的变量也就完成了垃圾回收
当遇到闭包时,外部函数执行完毕但内部函数还未执行,如果内部函数使用了外部环境中的变量,在外部函数出栈时被引用的变量不会被清除,而是作为单独的闭包作用域,存在于内部函数的作用域链上
当遇到
setTimeout
等定时任务时,将回调函数放入事件表(Event Table
)并由定时器线程开启一个定时任务,当定时结束后从事件表取出回调并加入消息队列当遇到
Promise
等微任务时,将微任务事件加入事件表,通过网络进程(或其他)处理异步任务后取出事件并加入宏任务对应的的微任务队列(Micro Task Queue
)中当前宏任务执行完成后,遍历其对应的微任务队列,依次执行其中的任务(期间产生的宏任务加入消息队列,产生的微任务还是加入当前微任务队列)
在代码执行过程中,垃圾回收器会穿插的进行垃圾标记(称为增量标记,避免阻塞主线程)与清除和整理。标记时会遍历调用栈,如果堆中的内存未被任何变量引用则需要回收
- 标记后,新生代区(新时代区内部分为对象区域、空闲区域两部分)会将不需要清除的对象移到空闲区(这个过程完成了内存整理,并且超过两次未被回收的对象会移入老生代区),全部移动之后清除对象区中的内存,之后交换两个区域,完成垃圾回收。
- 老生代区在标记后会先清理掉垃圾对象,然后将不需要回收的对象移至内存一侧(完成内存整理),完成垃圾回收。
一次事件循环完成,如果宏任务中进行了页面修改相关的操作,会进行一系列的渲染操作。之后取出下一个宏任务进行新一轮事件循环