事件循环(Event Loop), 是每个 JS 开发者都会接触到的概念, 但是刚接触时可能会存在各种疑惑.
众所周知, JS 是单线程的, 即同一时间只能运行一个任务. 一般情况下这不会引发问题, 但是如果我们有一个耗时较多的任务, 我们必须等该任务执行完毕才能进入下一个任务, 然而等待的这段时间常常让我们无法忍受, 因为我们这段时间什么都不能做, 包括页面也是锁死状态.
好在, 时代在进步, 浏览器向我们提供了 JS 引擎不具备的特性: web API.Web API 包括 DOM API, 定时器, HTTP 请求等特性, 可以帮助我们实现异步, 非阻塞的行为. 我们可以通过异步执行任务的方法来解决单线程的弊端, 事件循环为此而生.
提问 QAQ: 为什么 JavaScript 是单线程的?
多个线程表示您可以同时独立执行程序的多个部分. 确定一种语言是单线程还是多线程的最简单方法是看它拥有有多少个调用堆栈. JS 只有一个, 所以它是单线程语言.
将 JS 设计为单线程是由其用途运行环境等因素决定的, 作为浏览器脚本语言, JS 的主要用途是与用户互动, 以及操作 DOM. 这决定了它只能是单线程, 否则会带来很复杂的同步问题. 同时, 单线程执行效率高.
1. Event Loop 旧印象
大家熟悉的关于事件循环的机制说法大概是: 主进程执行完了之后, 每次从任务队列里取一个任务执行. 如图所示, 所有的任务分为同步任务和异步任务, 同步任务直接进入任务队列 -->主程序执行; 异步任务则会挂起, 等待其有返回值时进入任务队列从而被主程序执行. 异步任务会通过任务队列的机制 (先进先出的机制) 来进行协调. 具体如图所示:
同步和异步任务分别进入不同的执行环境, 同步的进入主线程, 即主执行栈, 异步的进入任务队列. 主线程内的任务执行完毕为空, 会去任务队列读取对应的任务, 推入主线程执行. 上述过程的不断重复就是我们所熟悉的 Event Loop (事件循环). 但是 promise 出现之后, 这个说法就不太准确了.
2. Event Loop 后印象
2.1 理论
这里首先用一张图展示 JavaScript 的事件循环:
直接看这张图, 可能黑人问号已经出现在同学的脑海...
这里将 task 分为两大类, 分别是 macroTask(宏任务)和 microTask(微任务). 一次事件循环: 先运行 macroTask 队列中的一个, 然后运行 microTask 队列中的所有任务. 接着开始下一次循环(只是针对 macroTask 和 microTask, 一次完整的事件循环会比这个复杂的多).
那什么是 macroTask? 什么是 microTask 呢?
JavaScript 引擎把我们的所有任务分门别类, 一部分归为 macroTask, 另外一部分归为 microTack, 下面是类别划分:
- macroTask:
- setTimeout
- setInterval
- setImmediate
- requestAnimationFrame
- I/O
- UI rendering
- microTask:
- process.nextTick
- Promise
- Object.observe
- MutationObserver
我们所熟悉的定时器就属于 macroTask, 仅仅了解 macroTask 的机制还是不够的. 为直观感受两种队列的区别, 下面上代码进行实践感知.
2.2 实践
以 setTimeout,process.nextTick,promise 为例直观感受下两种任务队列的运行方式.
- console.log('main1');
- process.nextTick(function() {
- console.log('process.nextTick1');
- });
- setTimeout(function() {
- console.log('setTimeout');
- process.nextTick(function() {
- console.log('process.nextTick2');
- });
- }, 0);
- new Promise(function(resolve, reject) {
- console.log('promise');
- resolve();
- }).then(function() {
- console.log('promise then');
- });
- console.log('main2');
别着急看答案, 先以上面的理论自己想想, 运行结果会是啥?
最终结果是这样的:
- main1
- promise
- main2
- process.nextTick1
- promise then
- // 第二次事件循环
- setTimeout
- process.nextTick2
process.nextTick 和 promise then 在 setTimeout 前面输出, 已经证明了 macroTask 和 microTask 的执行顺序. 但是有一点必须要指出的是. 上面的图容易给人一个错觉, 就是主进程的代码执行之后, 会先调用 macroTask, 再调用 microTask, 这样在第一个循环里一定是 macroTask 在前, microTask 在后.
但是最终的实践证明: 在第一个循环里, process.nextTick1 和 promise then 这两个 microTask 是在 setTimeout 这个 macroTask 里之前输出的, 这是因为 Promises/A + 规范规定主进程的代码也属于 macroTask.
主进程这个 macroTask(也就是 main1,promise 和 main2)执行完了, 自然会去执行 process.nextTick1 和 promise then 这两个 microTask. 这是第一个循环. 之后的 setTimeout 和 process.nextTick2 属于第二个循环
别看上面那段代码好像特别绕, 把原理弄清楚了, 都一样 ~
requestAnimationFrame,Object.observe(已废弃) 和 MutationObserver 这三个任务的运行机制大家可以从上面看到, 不同的只是具体用法不同. 重点说下 UI rendering. 在 html 规范: event-loop-processing-model 里叙述了一次事件循环的处理过程, 在处理了 macroTask 和 microTask 之后, 会进行一次 Update the rendering, 其中细节比较多, 总的来说会进行一次 UI 的重新渲染.
3. 小结
总而言之, 记住一次事件循环: 先运行 macroTask 队列中的一个, 然后运行 microTask 队列中的所有任务. 接着开始下一次循环.
参考文献:
总是一知半解的 Event Loop
深入理解事件循环机制 https://zhuanlan.zhihu.com/p/87684858
JavaScript 运行机制
来源: https://www.cnblogs.com/rainbowly/p/13156570.html