共计 5818 个字符,预计需要花费 15 分钟才能阅读完成。
一、概述
js 引擎执行过程主要分为三个阶段,分别是语法分析,预编译和执行阶段,上篇文章我们介绍了语法分析和预编译阶段,那么我们先做个简单概括,如下:
1、语法分析:分别对加载完成的代码块进行语法检验,语法正确则进入预编译阶段;不正确则停止该代码块的执行,查找下一个代码块并进行加载,加载完成再次进入该代码块的语法分析阶段。
2、预编译:通过语法分析阶段后,进入预编译阶段,则创建变量对象(创建 arguments 对象(函数运行环境下),函数声明提前解析,变量声明提升),确定作用域链以及 this 指向。
如果对语法分析和预编译,还有疑问:javascript 引擎执行的过程的理解 – 语法分析和预编译阶段。
同步更新 sau 交流学习社区(nodeJSBlog):javascript 引擎执行的过程的理解 – 执行阶段(https://www.mwcxs.top/page/56…)
本文主要分析 js 引擎执行的第三个阶段–执行阶段,在分析之前我们先思考以下两个问题:
1、js 是单线程的,为了避免代码解析阻塞使用了异步执行,那么它的异步执行机制是怎么样的?
答:通过事件循环(Event Loop),理解了事件循环的原理就理解了 js 的异步执行机制,本文主要介绍。
2、js 是单线程的,那么是否代表参与 js 执行过程的线程就只有一个?
答:不是的,会有四个线程参与该过程,但是永远只有 JS 引擎线程在执行 JS 脚本程序,其他的三个线程只协助,不参与代码解析与执行。参与 js 执行过程的线程分别是:
(1)JS 引擎线程:也称为 JS 内核,负责解析执行 Javascript 脚本程序的主线程(例如 V8 引擎)。
(2)事件触发线程:归属于浏览器内核进程,不受 JS 引擎线程控制。主要用于控制事件(例如鼠标,键盘等事件),当该事件被触发时候,事件触发线程就会把该事件的处理函数推进事件队列,等待 JS 引擎线程执行。
(3)定时器触发线程:主要控制计时器 setInterval 和延时器 setTimeout,用于定时器的计时,计时完毕,满足定时器的触发条件,则将定时器的处理函数推进事件队列中,等待 JS 引擎线程执行。
注:W3C 在 HTML 标准中规定 setTimeout 低于 4ms 的时间间隔算为 4ms。
(4)HTTP 异步请求线程:通过 XMLHttpRequest 连接后,通过浏览器新开的一个线程,监控 readyState 状态变更时,如果设置了该状态的回调函数,则将该状态的处理函数推进事件队列中,等待 JS 引擎线程执行。
注:浏览器对同一域名请求的并发连接数是有限制的,Chrome 和 Firefox 限制数为 6 个,ie8 则为 10 个。
总结:永远只有 JS 引擎线程在执行 JS 脚本程序,其他三个线程只负责将满足触发条件的处理函数推进事件队列,等待 JS 引擎线程执行。
二、执行阶段
先分析一个典型的例子(来自 Tasks, microtasks, queues and schedules,建议英文基础好的阅读,非常不错的文章):
console.log(‘script start’);
setTimeout(function() {console.log(‘setTimeout’);}, 0);
Promise.resolve().then(function() {console.log(‘promise1’);}).then(function() {console.log(‘promise2’);});
console.log(‘script end’); 直接划分例子的代码结构,简单描述分析执行过程;
暂不解释该过程中的概念和原理,概念和原理将会在下面具体讲解如下:
1、宏任务(macro-task)
宏任务(macro-task),宏任务又按执行顺序分为同步任务和异步任务
(1)同步任务
console.log(‘script start’);console.log(‘script end’);(2)异步任务
setTimeout(function() {console.log(‘setTimeout’);}, 0);
2、微任务(micro-task)
Promise.resolve().then(function() {console.log(‘promise1’);}).then(function() {console.log(‘promise2’);});
在 JS 引擎执行过程中,进入执行阶段后,代码的执行顺序如下:
宏任务(同步任务) –> 微任务 –> 宏任务(异步任务)
输出结果:
script startscript endpromise1promise2setTimeout
进入 ES6 或 Node 环境中,JS 的任务分为两种,分别是宏任务(macro-task)和微任务(micro-task),在最新的 ECMAScript 中,微任务称为 jobs,宏任务称为 task,他们的执行顺序如上。可能很多人对上面的分析并不理解,那么我们接下来继续对上面例子进行详细分析。
2.1 宏任务
宏任务(macro-task)可分为同步任务和异步任务:
1、同步任务指的是在 JS 引擎主线程上按顺序执行的任务,只有前一个任务执行完毕后,才能执行后一个任务,形成一个执行栈(函数调用栈)。
2、异步任务指的是不直接进入 JS 引擎主线程,而是满足触发条件时,相关的线程将该异步任务推进任务队列(task queue),等待 JS 引擎主线程上的任务执行完毕,空闲时读取执行的任务,例如异步 Ajax,DOM 事件,setTimeout 等。
理解宏任务中同步任务和异步任务的执行顺序,那么就相当于理解了 JS 异步执行机制–事件循环(Event Loop)。
2.1.1 事件循环
事件循环可以理解成由三部分组成,分别是:
1、主线程执行栈
2、异步任务等待触发
3、任务队列
任务队列 (task queue) 就是以队列的数据结构对事件任务进行管理,特点是先进先出,后进后出。
这里直接引用一张著名的图片(参考自 Philip Roberts 的演讲《Help, I’m stuck in an event-loop》),帮助我们理解,如下:
在 JS 引擎主线程执行过程中:
1、首先执行宏任务的同步任务,在主线程上形成一个执行栈,可理解为函数调用栈。
2、当执行栈中的函数调用到一些异步执行的 API(例如异步 Ajax,DOM 事件,setTimeout 等 API),则会开启对应的线程(Http 异步请求线程,事件触发线程和定时器触发线程)进行监控和控制。
3、当异步任务的事件满足触发条件时,对应的线程则会把该事件的处理函数推进任务队列 (task queue) 中,等待主线程读取执行。
4、当 JS 引擎主线程上的任务执行完毕,则会读取任务队列中的事件,将任务队列中的事件任务推进主线程中,按任务队列顺序执行
5、当 JS 引擎主线程上的任务执行完毕后,则会再次读取任务队列中的事件任务,如此循环,这就是事件循环(Event Loop)的过程。
如果还是不能理解,那么我们再次拿上面的例子进行详细分析,该例子中宏任务的代码部分是:
console.log(‘script start’);
setTimeout(function() {console.log(‘setTimeout’);}, 0);
console.log(‘script end’); 代码执行过程如下:
1、JS 引擎主线程按代码顺序执行,当执行到 console.log(‘script start’);,JS 引擎主线程认为该任务是同步任务,所以立刻执行输出 script start,然后继续向下执行。
2、JS 引擎主线程执行到 setTimeout(function() {console.log(‘setTimeout’); }, 0);,JS 引擎主线程认为 setTimeout 是异步任务 API,则向浏览器内核进程申请开启定时器线程进行计时和控制该 setTimeout 任务。由于 W3C 在 HTML 标准中规定 setTimeout 低于 4ms 的时间间隔算为 4ms,那么当计时到 4ms 时,定时器线程就把该回调处理函数推进任务队列中等待主线程执行,然后 JS 引擎主线程继续向下执行。
3、JS 引擎主线程执行到 console.log(‘script end’);,JS 引擎主线程认为该任务是同步任务,所以立刻执行输出 script end。
4、JS 引擎主线程上的任务执行完毕(输出 script start 和 script end)后,主线程空闲,则开始读取任务队列中的事件任务,将该任务队里的事件任务推进主线程中,按任务队列顺序执行,最终输出 setTimeout,所以输出的结果顺序为 script start script end setTimeout。
以上便是 JS 引擎执行宏任务的整个过程。
理解该过程后,我们做一些拓展性的思考:
我们都知道 setTimeout 和 setInterval 是异步任务的定时器,需要添加到任务队列等待主线程执行,那么使用 setTimeout 模拟实现 setInterval,会有区别吗?
答案是有区别的,我们不妨思考一下:
1、setTimeout 实现 setInterval 只能通过递归调用。
2、setTimeout 是在到了指定时间的时候就把事件推到任务队列中,只有当在任务队列中的 setTimeout 事件被主线程执行后,才会继续再次在到了指定时间的时候把事件推到任务队列,那么 setTimeout 的事件执行肯定比指定的时间要久,具体相差多少跟代码执行时间有关。
3、setInterval 则是每次都精确的隔一段时间就向任务队列推入一个事件,无论上一个 setInterval 事件是否已经执行,所以有可能存在 setInterval 的事件任务累积,导致 setInterval 的代码重复连续执行多次,影响页面性能。
综合以上的分析,使用 setTimeout 实现计时功能是比 setInterval 性能更好的。当然如果不需要兼容低版本的 IE 浏览器,使用 requestAnimationFrame 是更好的选择。
我们继续再做进一步的思考,如下:
高频率触发的事件(例如滚动事件)触发频率过高会影响页面性能,甚至造成页面卡顿,我们是否可以利用计时器的原理进行优化呢?
是可以的,我们可以利用 setTimeout 实现计时器的原理,对高频触发的事件进行优化,实现点在于将多个触发事件合并成一个,这就是防抖和节流,本文先不做具体讲解,大家可以自行研究,有机会我再另开文章分析。
2.2 微任务
微任务是在 es6 和 node 环境中出现的一个任务类型,如果不考虑 es6 和 node 环境的话,我们只需要理解宏任务事件循环的执行过程就已经足够了,但是到了 es6 和 node 环境,我们就需要理解微任务的执行顺序了。微任务(micro-task)的 API 主要有:Promise,process.nextTick
这里我们直接引用一张流程图帮助我们理解,如下:
在宏任务中执行的任务有两种,分别是同步任务和异步任务,因为异步任务会在满足触发条件时才会推进任务队列(task queue),然后等待主线程上的任务执行完毕,再读取任务队列中的任务事件,最后推进主线程执行,所以这里将异步任务即任务队列看作是新的宏任务。执行的过程如上图所示:
1、执行宏任务中同步任务,执行结束。
2、检查是否存在可执行的微任务,有的话执行所有微任务,然后读取任务队列的任务事件,推进主线程形成新的宏任务;没有的话则读取任务队列的任务事件,推进主线程形成新的宏任务。
3、执行新宏任务的事件任务,再检查是否存在可执行的微任务,如此不断的重复循环。
这就是加入微任务后的详细事件循环,如果还没有理解,那么们对一开始的例子做一个全面的分析,如下:
console.log(‘script start’);
setTimeout(function() {console.log(‘setTimeout’);}, 0);
Promise.resolve().then(function() {console.log(‘promise1’);}).then(function() {console.log(‘promise2’);});
console.log(‘script end’); 执行过程如下:
1、代码块通过语法分析和预编译后,进入执行阶段,当 JS 引擎主线程执行到 console.log(‘script start’);,JS 引擎主线程认为该任务是同步任务,所以立刻执行输出 script start,然后继续向下执行。
2、JS 引擎主线程执行到 setTimeout(function() {console.log(‘setTimeout’); }, 0);,JS 引擎主线程认为 setTimeout 是异步任务 API,则向浏览器内核进程申请开启定时器线程进行计时和控制该 setTimeout 任务。由于 W3C 在 HTML 标准中规定 setTimeout 低于 4ms 的时间间隔算为 4ms,那么当计时到 4ms 时,定时器线程就把该回调处理函数推进任务队列中等待主线程执行,然后 JS 引擎主线程继续向下执行。
3、JS 引擎主线程执行到 Promise.resolve().then(function() {console.log(‘promise1’); }).then(function() {console.log(‘promise2’); });,JS 引擎主线程认为 Promise 是一个微任务,这把该任务划分为微任务,等待执行。
4、JS 引擎主线程执行到 console.log(‘script end’);,JS 引擎主线程认为该任务是同步任务,所以立刻执行输出 script end。
5、主线程上的宏任务执行完毕,则开始检测是否存在可执行的微任务,检测到一个 Promise 微任务,那么立刻执行,输出 promise1 和 promise2
6、微任务执行完毕,主线程开始读取任务队列中的事件任务 setTimeout,推入主线程形成新宏任务,然后在主线程中执行,输出 setTimeout
最后输出结果:
script startscript endpromise1promise2setTimeout
三、总结
以上便是 JS 引擎执行的全部过程,JS 引擎的执行过程其实并不复杂,只要多思考多研究就可以理解,理解该过程后可以在一定程度上提高对 JS 的认识。
四、参考文献
Tasks, microtasks, queues and schedules