深入理解事件循环机制event loop
深入理解事件循环机制event loop
概念提出:
众所周知,JavaScript 是一门单线程语言,虽然在 html5 中提出了 Web-Worker ,但这并未改变 JavaScript 是单线程这一核心。可看HTML规范中的这段话:
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.
为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,用户引擎必须使用 event loops。Event Loop 包含两类:一类是基于 Browsing Context ,一种是基于 Worker ,二者是独立运行的。
代码示例
下面本文用一个例子,着重讲解下基于 Browsing Context 的事件循环机制。
来看下面这段 JavaScript 代码:
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');
测试结果如下:
说明:
因为js中是单线程,所以为了协调这些同步任务和异步任务任务,js有一个叫做**任务队列**的概念
任务队列说明
总所周知,所有的任务可以分为同步任务和异步任务:
-
同步任务
同步任务,顾名思义,就是立即执行的任务,同步任务一般会直接进入到主线程中执行
-
异步任务
异步任务,就是异步执行的任务,比如
ajax
网络请求,setTimeout
定时函数等都属于异步任务,异步任务会通过任务队列( Event Queue )的机制来进行协调。
易混淆的概念:
- 一个事件循环有一个或多个任务队列。一个 任务队列是一组的任务。
- 任务队列是集合,而不是队列,因为事件循环处理模型的第一步是从所选队列中获取第一个可运行的 任务,而不是让第一个任务出队。
- 微任务队列不是任务队列。
任务队列工作流程图
流程说明:
同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入 Event Queue 。主线程内的任务执行完毕为空,会去 Event Queue 读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。
事件循环说明:
在事件循环中,每进行一次循环操作称为tick,通过阅读规范可知,每一次 tick 的任务处理模型是比较复杂的,其关键的步骤可以总结如下:
- 在此次 tick 中选择最先进入队列的任务( oldest task ),如果有则执行(一次)
- 检查是否存在 Microtasks ,如果存在则不停地执行,直至清空Microtask Queue
- 更新 render(渲染)
- 主线程重复执行上述步骤
原文截图(附关键词说明):
翻译如下:
- 每个事件循环都有一个当前正在运行的任务,该任务可以是任务,也可以是空任务。
最初,这是空的。它用于处理可重入性。 - 每个事件循环都有一个微任务队列,这是一个最初为空的微任务队列。微任务是指通过队列(一种微任务算法)创建的任务的一种口语化方式。
- 每个事件循环都有一个执行微任务检查点布尔值,该布尔值最初为False。它用于防止重入调用执行微任务检查点算法。
- 每个窗口事件循环都有一个DOMHighResTimeStamp上次呈现机会时间,最初设置为零。
- 每个窗口事件循环都有一个DOMHighResTimeStamp上一个空闲周期开始时间,最初设置为零。
- 要获得窗口事件循环的相同循环窗口,请返回其相关代理的事件循环为循环的所有窗口对象。
事件循环(Event Loop)流程说明(简洁):
task任务说明(micro task以及macro task说明)
规范中规定,task分为两大类, 分别是 Macro Task (宏任务)
和 Micro Task(微任务)
, 并且每个宏任务结束后, 都要清空所有的微任务,这里的 Macro Task也是我们常说的 task ,后面文章中所提及的task皆看做宏任务( macro task)。
- (macro)task 主要包含:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
- microtask主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)
setTimeout
/Promise
等API便是任务源,而进入任务队列的是由他们指定的具体执行任务。来自不同任务源的任务会进入到不同的任务队列。其中 setTimeout 与 setInterval 是同源的。
分析示例代码
示例代码:
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');
- 整体 script 作为第一个宏任务进入主线程,遇到 console.log,输出 script start
- 遇到 setTimeout,其回调函数被分发到宏任务 Event Queue 中
- 遇到 Promise,其 then函数被分到到微任务 Event Queue 中,记为 then1,之后又遇到了 then 函数,将其分到微任务 Event Queue 中,记为 then2
- 遇到 console.log,输出 script end
至此,Event Queue 中存在三个任务,如下表:
宏任务 | 微任务 |
---|---|
setTimeout() | then1 、then2 |
- 执行微任务,首先执行then1,输出 promise1, 然后执行 then2,输出 promise2,这样就清空了所有微任务
- 执行 setTimeout 任务,输出 setTimeout 至此,输出的顺序是:script start, script end, promise1, promise2, setTimeout
难度++(思考):
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
console.log('then1')
})
console.log('script end');
结果如图:
解释:
总结:
- 从规范来看,microtask 优先于 task 执行,所以如果有需要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行。(示例就是最好的证明)
- 最后的最后,记住,JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。