前端基础 | js执行过程你了解多少?
说实话,之前真的不太了解这一块,我想也是大部分前端的问题吧,趁着刷题,巩固下基础知识。
js是单线程语言:
即:在浏览器中只有一个线程在执行js脚本代码。
这里所说的js是单线程,并不是说在js执行过程中只有一个线程。其实有四个线程,包括:
- JS引擎线程
- 事件触发线程
- 定时器触发线程
- HTTP异步请求线程
但永远只有 JS引擎线程 在执行js脚本,其他三个只是协助,不参与脚本解析和执行。
那么问题来了:为什么 JS 被设计成单线程?
js是单线程,但是代码解析不会发生阻塞
js是异步执行的,通过事件循环(
Event Loop
)的方式实现。
js引擎执行分三个阶段
注:浏览器首先按顺序加载由
<script>
标签分割的js代码块,加载js代码块完毕后,立刻进入以下三个阶段,然后再按顺序查找下一个代码块,再继续执行以下三个阶段,无论是外部脚本文件(不异步加载)还是内部脚本代码块,都是一样的原理,并且都在同一个全局作用域中。
- 语法分析
- 预编译阶段
- 执行阶段
语法分析
分析脚本代码块语法是否正确,正确则进入「预编译阶段」,否则抛出 SyntaxError
, 停止该代码块代码继续执行,然后继续查找下一个代码块。
预编译阶段
js的运行环境(执行上下文)主要有三种:
- 全局环境(js代码加载完毕后,进入预编译阶段即进入了全局环境)
- 函数环境(函数执行时进入该函数环境,不同函数的函数环境不同)
- eval环境(不建议使用,会有安全,性能等问题)
执行栈,又称调用栈,用来存贮代码执行期间创建的所有执行上下文。是一个遵循后进先出(LIFO)的结构。栈底永远是全局执行上下文,栈顶是当前执行上下文。
执行上下文分两个阶段创建:1)创建执行上下文; 2)执行阶段
创建执行上下文
- 创建变量对象
- 创建作用域链
- 确定
this
指向
创建变量对象
主要过程如下:
- 创建
Augments
对象:检查当前上下文中的参数,建立对象的属性与值,仅在函数环境(非箭头函数)执行,全局环境没有这个过程。 - 检查
Funchtion
函数声明,创建属性:按代码顺序查找,将找到的函数提前声明,若函数不存在则新建立属性和属性值(指向该函数内存地址的引用);若存在,则直接覆盖原来的。 - 检查
var
变量,声明创建属性:按代码顺序查找,将找到的变量提前声明,如果变量不存在,则赋值为:undefined
。若存在,则忽略该声明。
注:在全局环境中,
window
对象就是全局执行上下文的变量对象,所有的变量和函数都是window
对象的属性方法。
所以函数声明提前和变量声明提升是在创建变量对象中进行的,且函数声明优先级高于变量声明。
变量提升:在创建阶段,函数声明存储在环境中,而变量会被设置为 undefined
(在 var
的情况下)或保持未初始化(在 let
和 const
的情况下)。所以这就是为什么可以在声明之前访问 var
定义的变量(尽管是 undefined
),但如果在声明之前访问 let
和 const
定义的变量就会提示引用错误的原因。
「变量对象」转化为「活动对象」后才能进行访问
创建作用域链
作用域链由当前执行环境的变量对象和上层的一系列活动对象组成,保证了当前执行环境对符合访问权限的变量和函数的有序访问。
例:
1 | var num = 30; |
在上面的例子中,当执行到调用 innerTest
函数,进入 innerTest
函数环境。全局执行上下文和 test
函数执行上下文已进入执行阶段,innerTest
函数执行上下文在预编译阶段创建变量对象,所以他们的活动对象和变量对象分别是 AO(global)
,AO(test)
和 VO(innerTest)
,而 innerTest
的作用域链由当前执行环境的变量对象(未进入执行阶段前)与上层环境的一系列活动对象组成,如下:
1 | innerTestEC = { |
- 作用域链的第一项永远是当前作用域(当前上下文的变量对象或活动对象);
- 最后一项永远是全局作用域(全局执行上下文的活动对象);
- 作用域链保证了变量和函数的有序访问,查找方式是沿着作用域链从左至右查找变量或函数,找到则会停止查找,找不到则一直查找到全局作用域,再找不到则会抛出引用错误。
确定 this
指向
全局环境下指向window
函数环境下需根据执行环境和执行方法确定
执行阶段
js进入执行阶段后,代码执行顺序如下:
宏任务(同步任务) –> 微任务 –> 宏任务(异步任务)
1.宏任务:宏任务又按执行顺序分为同步任务和异步任务
- 同步任务: 在JS引擎主线程上按顺序执行的任务,只有前一个任务执行完毕后,才能执行后一个任务,形成一个执行栈(函数调用栈)
- 异步任务: 不直接进入JS引擎主线程,而是满足触发条件时,相关的线程将该异步任务推进任务队列(
task queue
),等待JS引擎主线程上的任务执行完毕,空闲时读取执行的任务,例如异步Ajax
,DOM
事件,setTimeout
等。
理解宏任务中同步任务和异步任务的执行顺序,那么就相当于理解了JS异步执行机制–事件循环( Event Loop
)
事件循环可以理解为由三部分组成:主线程执行栈、异步任务等待触发、任务队列
在JS引擎主线程执行过程中:
- 首先执行宏任务的同步任务,在主线程上形成一个执行栈,可理解为函数调用栈;
- 当执行栈中的函数调用到一些异步执行的
API
(例如异步Ajax
,DOM
事件,setTimeout
等API
),则会开启对应的线程(Http
异步请求线程,事件触发线程和定时器触发线程)进行监控和控制 - 当异步任务的事件满足触发条件时,对应的线程则会把该事件的处理函数推进任务队列(
task queue
)中,等待主线程读取执行 - 当JS引擎主线程上的任务执行完毕,则会读取任务队列中的事件,将任务队列中的事件任务推进主线程中,按任务队列顺序执行
- 当JS引擎主线程上的任务执行完毕后,则会再次读取任务队列中的事件任务,如此循环,这就是事件循环(
Event Loop
)的过程
2.微任务:是在 es6
和 node
环境中出现的一个任务类型.
微任务的
API
主要有:Promise
,process.nextTick
示例:
1 | console.log('script start'); |
输出结果:
1 | script start |
以上就是js执行的过程
参考文章: