事件循环(event loop)是一个在 JavaScript 常被提起的概念, 它存在于浏览器也存在于 Node.js 中, 由于它复杂的运行机制使人难以琢磨, 更是成为面试时候的必考题目.javascript
本篇文章的内容源自我对网络上的一些介绍 "事件循环" 的演讲视频中内容的总结, 其中大多数来自于 jsconf
. 在每一章中我都附上的视频地址, 建议之间直接观看视频相于文字来讲视频更容易理解.css
https://youtu.be/8aGhZQkoFbQ
咱们都知道JavaScript是一个单线程的编程语言, 这意味着它只能有一个调用栈, 执行过程当中每次只能作一件事情.html
为了充分理解, 咱们来例举一个简单的例子:java
function fun1() { return 'fun1' } function fun2() { return 'fun2' + fun1(); } function fun3() { console.log(fun2()); } fun3();
一旦代码执行调用栈中函数压入的顺序以下:node
+----------+ +----------+ +----------+ | stack | | stack | | stack | | | | | | | | | | | | | | | | | | +------+ | | | | | | | fun3 | | | | | | | +------+ | | | | | | | | | | +------+ | | +------+ | | | | | fun2 | | | | fun2 | | | | | +------+ | | +------+ | | | | | | | | +------+ | | +------+ | | +------+ | | | fun1 | | | | fun1 | | | | fun1 | | | +------+ | | +------+ | | +------+ | | | | | | | +----------+ +----------+ +----------+
位于栈顶的函数(fun3)执行完成后调用栈会把他弹出:linux
+----------+ +----------+ +----------+ | stack | | stack | | stack | | | | | | | | | | | | | | | | | | | | 💥 | | | | | | | | | | | | | | | | | | +------+ | | | | | | | fun2 | | | 💥 | | | | +------+ | | | | | | | | | | | | +------+ | | +------+ | | | | | fun1 | | | | fun1 | | | ✨ | | +------+ | | +------+ | | | | | | | | | +----------+ +----------+ +----------+
调用栈被JavaScript引擎使用和监视, 因此当咱们在函数中抛出了一个错误可是没有对应的 try/catch
语句的时候:程序员
function fun1() { throw new Error('Warning: Nuclear Missile Launched') // 抛出一个错误可是没有对于的 try/catch 语句 } function fun2() { return 'fun2' + fun1(); } function fun3() { console.log(fun2()); } fun3();
能够在控制台中的报错中看到调用栈的信息:web
可是调用栈也是脆弱的若是咱们建立一个没法中断调用函数, 那么调用栈会瞬间爆炸💥这种行为被称为 "栈溢出". 可喜可贺的是JavaScript会监视调用栈, 一旦调用栈出现 "栈溢出" JavaScript 会终止代码的执行而且抛出错误:面试
function foobar() { foobar(); } foobar();
抛出 "栈溢出" 错误:ajax
在 Web 开发中咱们常常要会听到要避免浏览器阻塞, "阻塞" 这个词感受上有点像咱们常说的 "电脑卡住了" 中的 "卡".
实际上阻塞并无一个严格的定义, JavaScript 的运行速度是很快的, 执行几个简单的 "console.log" 你没法体会到这其中所花费的时间, 若是调用栈中正在执行的函数花费了大量的时间, 咱们感受浏览器被卡住了, 体验不流畅此时咱们说, 浏览器被阻塞了.
可是浏览器不只仅执行 JavaScript 代码还会异步的加载外部资源文件, 这些加载的流程也能够被 JavaScript 所控制, 例如咱们经过 JavaScript 发起一个同步的网络请求:
var oReq = new XMLHttpRequest(); oReq.onload = function(){}; oReq.open("get", "https://xxx.com/", false); // false 表示同步发送请求(这种方式已经不在被推荐使用仅仅用于示例) oReq.send(); alert('running!'); // 只有请求完成后 alert 才会执行
这个请求有可能须要 20ms 或者 300ms 甚至更长, 在请求的过程当中浏览器会一直等待请求完成甚至会中止页面的渲染, 咱们能够明显的感觉到浏览器卡住了, 这是典型的阻塞.
浏览器将全部可能花费大量时间等待的操做都提供了对应的异步接口, 这种解决方式被称为 "异步函数" 或者 "回调函数" 或者 "异步回调" 等.
一个典型的例子以下:
console.log('hello'); setTimeout(function foobar(){ console.log('delay'); },1000); console.log('world');
咱们都知道这段代码会输出的顺序是:
那么浏览器究竟是如何解释这段代码的呢, 咱们能够观察浏览器的调用栈:
console.log('hello'); // 压入栈中执行 console.log('hello'); // 执行完成栈弹出 setTimeout // 压入栈中执行 setTimeout // 执行完成栈弹出 console.log('world'); // 压入栈中执行 console.log('world'); // 执行完成栈弹出 // -------1000ms事后------ foobar // 压入栈中执行 console.log('delay'); // 压入栈中执行 console.log('delay'); // 执行完成弹出 foobar // 执行完成弹出
最神奇的事情出现了 setTimeout
执行完成后就被调用栈弹出了, 可是不知何故 1000ms 后 setTimeout
中的 foobar
被神奇的唤醒了, 这是怎么回事?
在 JavaScript 中执行元素绑定事件, 使用 setTimeout
设置一个延时执行, 或者发起一次网络请求. 这些都是web开发者的屡见不鲜, 对于咱们来讲这些内容就是 JavaScript 的一部分了, 可是实际上这些内容并无在 ECMAScript 制定的标准中, 包括咱们讨论的 "事件循环" 机制它也没有存在于规范中也就是说这个机制是独立于 JavaScript 引擎的功能.
这些API被称为 webapi
它们有本身的规范和实现与 JavaScript 这门语言没有关系, 在这份来自于MDN的页面上列举了大部分的API.
咱们以前提到了 JavaScript 因为其单线程的特性只能在同一时间执行同一间事情, 这是正确的, 可是浏览器不只仅拥有解释 JavaScript 脚本的引擎, 还有一堆其余的程序来处理诸如 DOM
XmlHttpRequest
setTimeout
这些任务.
这些程序各司其职完成本身负责的部分, 它们是独立于 JavaScript 引擎以外的内容, 这些程序可能运行在独立线程上或者进程上它们经过webapi
来和 JavaScript引擎进行通讯, 因此咱们须要经过 "回调" 的方式进行异步编程.
而 事件循环 用于管理这些异步任务在合适的时机执行.
setTimeout
做为 webapi
典型的例子, 咱们来观察一下它是如何运行的, 首先 setTimeout
一旦被执行便将函数钩子移交给对应的 webapi
而且开始计时:
+--------------------------------+ +---------------------+ | | |webapis | | | | +----------------+ | | setTimeout(function foobar() { | | | | | | console.log('delay') | +-> | | timer-0 -- cb()| | | },0); | | | | | | | | +----------------+ | | | | | +--------------------------------+ +---------------------+
因为咱们的倒计时是0, 因此计时会当即完成, webapi
将函数钩子移交给 任务队列 .
+--------------------------------+ +----------+ | | |webapis | | | | | | setTimeout(function foobar() { | | | | console.log('delay') | | | +-+ | },0); | | | | | | | | | | | | | | +--------------------------------+ +----------+ | | +-------------------------------------------------+ | | task queue | | | +--------------+ | | | | | | | | | cb() | | <-+ | | | | | +--------------+ | | | +-------------------------------------------------+
接下来终于轮到 事件循环 上场了, 事件循环完成一件很是简单的工做, 它判断若是:
那么就将这个任务移入到调用栈中执行:
+--------------------------------+ +------------------+ | | | stack | | | | +--------------+ | | setTimeout(function foobar() { | | | | | | console.log('delay') | | | cb - foobar | | | },0); | | | | | | | | +--------+-----+ | | | | ^ | +--------------------------------+ | | | event loop ⏳ | | | +----+---------------------------+ | | | | | | | | | | | | | | | | +-- task ------> push -------------------+ | | | | | +--------------------------------+ +------------------+
而后往复循环这个过程, 这就是事件循环的基本流程.
如今咱们来提出一个 ajax
的例子, 尝试一下你能说出他的执行流程吗:
console.log('hello'); $.get('https:www.google.com',function foobar(){ console.log('callback') }); console.log('world');
他的执行流程以下:
webapi
中的 XHR
发起了网络请求, 并保存其函数钩子webapi
将函数钩子移动到任务队列中事件循环--> 检查调用栈是否为空 检查任务队列中是否有任务 (事件循环一直存在并不是在只在此刻进行检查)
https://youtu.be/cCOL7MC4Pl0
在上一节中咱们提到了 JavaScript 是单线程的若是花费大量的时间运行 JavaScript 那么就会阻塞浏览器, 致使浏览器没法完成其余工做, 而且初次了解了 "事件循环" 是如何解决此问题的. 在这一节中咱们会更加了解另一个话题 "事件循环" 与页面渲染之间的关系.
实际上事件循环的做用范围可能超乎你的想象, 全部的DOM事件实际上都是受到了 "事件循环" 控制的, 除此之外还包括包括网络请求, IO操做等等. 由于在背后这些事件的触发者实际上都是将与事件有关的信息放入到了 "任务队列" 中真正让这些内容被执行的其实是 "事件循环", 还记得 "事件循环" 是如何工做的吗?
当下列条件成立时, "事件循环" 会将最近的任务移送到 "调用栈" 中:
可是浏览器不只能够执行 JavaScript 还会能够处理页面渲染, 咱们知道若是进行大量的 JavaScript 计算会阻塞页面渲染, 这里到底有何种联系? 在此以前咱们先来了解一下基本的渲染概念.
咱们在页面中动态的修改样式界面咱们能够看到实时的效果, 因此给咱们一种修改样式是同步的操做的错觉, 实际上浏览器在背后进行了优化, 重复多此的样式操做会被进行合并而后由浏览器决定一个合适的时机而后统一更新:
element.style.transition = "transform 1s"; element.style.transform = "translateX(100px)"; element.style.transform = "translateX(500px)";
应用了样式的元素并不会横向在 100px 和 500px 之间来回移动而是直接移动到了 500px, 浏览器抛弃了旧的无用的样式修改.
因此说修改样式并非实时的, 这种批量更新样式的机制致使它和 "事件循环" 之间产生了一些微妙的关联, 在了解这些关联前咱们先来了解一下浏览器的基本渲染机制.
元素样式决定渲染的结果, 从一堆代码转为可视化的界面经历了许多环节, 这里咱们来简单的了解一下其中的几个关键步骤:
在下面的这个例子中使用了一个 video
来表示页面进行动态的持续的页面渲染, 这些嵌入的内容能够在不受 JavaScript 的干扰下影响页面的显示, 当点击按钮的时候 JavaScript 进入死循环:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>test</title> </head> <body> <video src="https://vjs.zencdn.net/v/oceans.mp4" controls autoplay></video> <button id="button">while true</button> <script> document.getElementById('button').addEventListener('click',()=>{ while(true); }); </script> </body> </html>
当咱们点击了按钮的时候:
while true
执行此时渲染工做在等待 JavaScript 执行完成, 可是 JavaScript 进入了无限循环中因此渲染工做就一直在等待中永远不会获得完成.
可是页面的渲染却受到了来自事件循环中的阻塞, 为何会这样?
解释这种行为的一个好的方式就是: 咱们不妨把页面的渲染过程也视为 "任务队列" 中的一个任务, 这个任务的建立者就是浏览器自己,它在一个合适的时机把渲染任务放入到任务队列中等待执行, 可是因为代码阻塞致使页面没法及时更新.
下列代码会形成阻塞吗:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>test</title> </head> <body> <video src="https://vjs.zencdn.net/v/oceans.mp4" controls autoplay></video> <button id="button">while true</button> <script> function loop(){ setTimeout(loop,0); } loop(); </script> </body> </html>
这段代码不只不会形成阻塞甚至不会形成栈溢出, 每次调用 loop
函数会向 "任务队列" 添加一个任务而后 loop
就会被弹出调用栈, 此时的调用栈就被清空了. 而负责控制任务队列的事件循环只有在调用栈为空的时候才能继续执行任务, 也就是说调用栈永远不会累加.
其次任务的执行并不影向任务队列添加内容, 在调用 setTimeout(loop,0)
后 "事件循环" 继续工做处理那些已经被填入到任务队列中在此以前的填入的其余事件以及页面的渲染, 直到 "任务队列" 中再次执行有关 loop
的任务.
试想一下你在页面上制做了一个动画效果使用以下代码:
function animate(){ // 修改样式 } setInterval(animate,1000/60);
你但愿动画能够达到 60fps 因此向 setInterval
传入了 1000/60
期待它能够每秒执行 60 次动画函数.
可是因为 setInterval
并不精确在一帧中可能执行了多此, 也可能一次也没有执行, 或者执行了一个耗时的任务致使浏览器没法在一帧中进行渲染操做.
咱们但愿每一帧中至少有一次渲染过程, 可是随机的任务执行会打乱理想中有规律的渲染过程, 致使渲染操做不能平均分布到每帧中:
而浏览器自己的渲染其实是很是智能且节约计算. 例如页面渲染频率自动和屏幕刷新率调整到一致, 当页面静止或者不可视的时候页面会中止渲染, 而使用 setInterval
等很难完美的和页面渲染过程相结合.
一个解决问题的办法就是使用 requestAnimationFrame
.
window.requestAnimationFrame() 告诉浏览器——你但愿执行一个动画,而且要求浏览器在下次重绘以前调用指定的回调函数更新动画
使用 requestAnimationFrame
咱们可使用浏览器的渲染逻辑将本来杂乱的渲染过程变得有序起来, 让浏览器决定什么时候进行渲染, 对于动画渲染这再好不过了, 如今有关动画的任务都被排列到了渲染任务的前面:
有关宏任务的概念在前面咱们已经涉及到了, 在浏览器中如下的几个异步 API 是宏任务相关:
而微任务是一个简单的概念, 咱们从微任务的设计历史来解释微任务为何这样执行.
好久前W3C给浏览器制定了一些API, 这些API用于监听DOM的变化:
element.addEventListener("DOMNodeInserted", function (ev) { // ... }, false);
可是这个API有着严重的性能问题, 只要修改元素的属性对应的事件就会被触发, 对同一个属性修改100次就会触发100次的事件, 另外事件具备冒泡的特性子元素的修改也会致使父元素触发该事件:
let i = 100; while(i--){ const span = document.createElement('span'); element.appendChild(span); // 追加元素触发事件一次 span.textContent = 'Test'; // 修改追加的元素的属性又触发一次事件 }
结果就是不管你在 DOMNodeInserted
回调中写多么简单的代码, 复杂的DOM操做致使事件就会被密集调用大大下降性能, 因此这个API被废弃了.
监听DOM修改的需求依然存在, 可是咱们但愿这个接口的表现就和渲染同样将DOM修改进行合并后只触发一次, 这个规范在DOM3中被推出他就是 MutationObserver
, 从而引入了 "微任务" 和 "微任务队列" 这个概念.
微任务是异步的咱们用个 Promise
来举例:
Promise.resolve().then(()=>console.log('hello world')); console.log('foobar');
输出:
foobar hello world
foobar
先于 hello world
输出这点证实了它. 虽然他是异步的但这不表明他必须遵循 "事件循环" 和 "渲染" 制定的规则. 相反 "微任务" 有本身的玩法.
一个典型的特征就是只有微任务队列清空后微任务才算执行完成, 咱们把以前的 "事件循环" 例子改成微任务版本:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>test</title> </head> <body> <video src="https://vjs.zencdn.net/v/oceans.mp4" controls autoplay></video> <button id="button">while true</button> <script> document.getElementById('button').addEventListener('click',()=>{ function loop(){ Promise.resolve().then(loop); } loop(); }); </script> </body> </html>
结果就是当点击按钮后页面渲染会中止浏览器进入到阻塞中, 缘由很简单 "微任务队列" 中永远有任务因此浏览器一直在等待 "微任务队列" 清空而后一直执行微任务, 不幸的是这是一个无尽的任务队列因此它永远没法执行完.
微任务的另一个特性就是 JavaScript 调用栈一旦被清空, 微任务队列中的任务执行会先于其余队列, 请观察下面的例子:
const button = document.getElementById('button'); button.addEventListener('click',()=>{ Promise.resolve().then(()=>console.log('Microtask 1')); console.log('task 1'); }); button.addEventListener('click',()=>{ Promise.resolve().then(()=>console.log('Microtask 2')); console.log('task 2'); });
点击按钮后输出顺序:
task 1 Microtask 1 task 2 Microtask 2
按钮点击后的执行流程以下:
"事件循环" 将第一个点击事件中的匿名函数压入调用栈
console.log('task 1')
console.log('Microtask 1')
"事件循环" 将第二个点击事件中的匿名函数压入调用栈
console.log('task 2')
console.log('Microtask 2')
不过若是要把这个例子稍稍修改一下状况却略有不一样:
const button = document.createElement('button'); button.addEventListener('click',()=>{ Promise.resolve().then(()=>console.log('Microtask 1')); console.log('task 1'); }); button.addEventListener('click',()=>{ Promise.resolve().then(()=>console.log('Microtask 2')); console.log('task 2'); }); button.click();
此次咱们手动触发 click
事件, 输出结果以下:
task 1 task 2 Microtask 1 Microtask 2
此次的执行流程为:
button.click()
被压入调用栈执行同步触发 'click' 事件并将第一个事件回调压入调用栈
console.log('Microtask 1')
console.log('task 1')
button.click
并未执行完成还在调用栈中同步触发 'click' 事件并将第二个事件回调压入调用栈
console.log('Microtask 2')
console.log('task 2')
button.click
从调用栈中弹出console.log('Microtask 1')
console.log('Microtask 2')
在浏览器中如下的 API 是微任务的任务源:
https://youtu.be/zphcsoSJMvM
回到 ms-dos
和 Apple os
的时代那时候的操做系统使用命令行界面, 计算机CPU只有一个核心, 操做系统同一时间下只能执行一件事情.经过操做界面告诉操做系统你要运行一个应用程序, 此时系统会中止运行并运行那个程序, 当应用程序执行完后又把执行权力交由操做系统.
这种设计有着很是大的限制, 你没法同时执行多件事情, 因而一种被称做协做多任务(cooperative multitasking)的机制出现了. 这种设计下你能够同时运行多个程序.
可是这种机制出现后表现各大操做系统的实现也不是十分完美, 在当时的 Mac OS
和 windows
操做系统中这种多程序执行的实现交由应用程序决定, 若是程序没有编写对应的代码那么这个程序会一直占用CPU资源, 若是一旦程序崩溃甚至会牵扯到系统, 致使系统崩溃.
后来这种机制被改成了抢占式多任务(preemptive multitasking), 运行哪一个程序由操做系统决定, 在切换应用程序的时候他会把正在运行中的程序暂停而后保留其状态存储到其余位置中, 而后加载另一个程序. 这项机制最初引用到了面向服务器的 Unix 系统, 在随后的时间里才应用到了使用 NT 内核的 windows2000 和同时期的 Mac OS 1004 的我的电脑中.
此时AMD刚刚发布了它的多核CPU, 为了充分利用多核CPU的性能, 均衡多线程(symmetric multi threading)技术诞生了, 该技术的实际应用被 intel 首先采用并从新命名为超线程(hyper threading). 该技术的主要原理是: CPU在执行任务的时候并不是一直满载执行, 这里有不少空闲资源能够利用, 而超线程容许一个CPU核心能够同时处理多件事情充分的利用CPU空闲资源.
在上文中咱们提到了两个基本的概念 "任务" 和 "线程", 实际上任务就是咱们常说的 "进程", 线程和进程的概念咱们就在这里不提了, 咱们须要提到的一点就是线程的 "竞态". 线程的执行是并行的线程间共享内容, 当两个线程操做同一个数据的时候会出现这种问题.
假设咱们有两个线程线程A向全局变量写入一个数据,线程B读取对应的全局变量,因为两个线程都是并行的因此线程A可能在线程B读取前进行了写入, 也有可能线程B先读取后线程A再写入, 每次运行都会获得不一样的结果. 这让程序充满了不肯定性也会致使不少bug, 许多语言都提供了线程安全的操做来避免问题, 不过即便是经验丰富的程序员每每也得仔细思考才能设计出线程安全的程序.
对于Node来讲解决的方式很是简单直接, 咱们使用单线程模型, 不容许你使用多线程模型(Node 11中添加了实验中的多线程支持).
Node.js 官网中有篇专门介绍事件循环的文章, 也是这节的核心, 这篇文章已经被翻译完成 👉访问链接. 原本打算放到这这篇文章中来的可是这样作致使本文太长了, 因此就移除出去了, 是一篇十分重要的文章, 对于理解 Node.js 中的事件循环相当重要.
https://youtu.be/gl9qHml-mKc
JavaScript 是单线程的这没有问题, 由于在 Node 中全部的 "JavaScript 脚本", "V8 引擎", "事件循环", 都运行在一个线程中这个线程被称为 "主线程".
可是这不意味着 Node 自己是单线程的由于 Node 还有其余部分. Node 的源码中还包括了 C++ 代码, C++部分拥有操做线程的能力, 这取决于你调用 JavaScript API的方式. 例如若是你调用了一个 Node 的 API, 这个 API 背后是由 C++ 代码提供支持的. 若是你同步的调用那么 C++ 代码会在主线程上执行. 若是你调用一个异步的 Node 接口, 那么 C++ 有可能会使用额外的线程来执行这个任务.
因此使用同步 API 那么 Node 就没有机会利用多线程的并行计算的特性来提高性能, 因此在任什么时候候都推荐使用异步的接口, 这样能够利用 Node 内部的线程机制进行优化执行效率.
在默认状况下 Node 会使用线程池线程池的容量为 4 (能够经过环境变量进行修改), 当须要线程的任务超过4个后, Node 会将任务放入到任务队列中. 一旦空闲线程出现 Node 会将任务从队列中取出放入到线程中执行.
图片:在 windows10 上使用命令行刚刚启动的 Node 就使用了12个线程:
有一些任务不依赖线程池而是依赖操做系统提供的接口, 例如 http.request
背后 C++ 会尽量的调用系统提供的异步接口 epoll(linux) kqueue(mac os), GetQueuedCompletionStatusEx(Windows)来将任务委托给操做系统去完成.
下列的列表中例举了异步 API 背后的运行机制:
Kernel Async
tcp/udp
sockets, serversThread Pool
Signal Handler(posix only)
Wait Thread(windows only)
不少人认为 Event loop 是独立的它运行在一个单独的线程中, 可是实际上 Event Loop 做为 JavaScript 部分的内容是和 JavaScript 同样运行在主线程上的.
若是你看过 Node.js 官方介绍事件循环的文章你就会知道(文章地址), 事件循环并非简单的栈或者队列的概念, 而是多个 "阶段" 的集合, 在不一样的 "阶段" 中用于保存任务的数据结构和执行逻辑是不一样的.
二者的不一样点主要在事件循环的执行机制上:
此外 Node 还有特殊的 process.nextTick
, 该 API 被视为微任务源之一, 可是执行方式和浏览器中的方式不一样. 在 Node11
后 Node 端的微任务执行效果开始和浏览器端趋同.
Node 和浏览器共有 setTimeout
和 Promise
这两个接口, 一个被视为宏任务源另外一个被视为微任务源, 咱们使用这两个 API
来作个小测试, 测试用例以下:
setTimeout(() => { console.log('setTimeout - 1'); setTimeout(() => { console.log('setTimeout - 1 - 1') }); new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 1 - then') new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 1 - then - then') }); }); }); setTimeout(() => { console.log('setTimeout - 2'); setTimeout(() => { console.log('setTimeout - 2 - 1') }); new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 2 - then') new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 2 - then - then') }); }); });
在浏览器端输出以下:
setTimeout - 1 setTimeout - 1 - then setTimeout - 1 - then - then setTimeout - 2 setTimeout - 2 - then setTimeout - 2 - then - then setTimeout - 1 - 1 setTimeout - 2 - 1
解释:
// 两个 setTimeout 中的任务都已经被到任务队列中. ------------- // 执行任务 - 1 setTimeout - 1 - setTimeout 添加了一个新的任务 - 3 - Promise 添加了一个新的微任务 // 第一个任务结束, 发现微任务存在, 执行微任务 setTimeout - 1 - then - Promise 添加了一个新的微任务 // 微任务执行完成, 发现新的微任务, 执行微任务 setTimeout - 1 - then - then // 微任务执行完成 // 执行任务 - 2 setTimeout - 2 - setTimeout 添加了一个新的任务 - 4 - Promise 添加了一个新的微任务 // 第一个任务结束, 发现微任务存在, 执行微任务 setTimeout - 2 - then - Promise 添加了一个新的微任务 // 微任务执行完成, 发现新的微任务, 执行微任务 setTimeout - 1 - then - then // 微任务执行完成 // 执行任务 3 setTimeout - 1 - 1 // 执行任务 4 setTimeout - 2 - 1
Node10 执行以下:
// 进入 timer 阶段 ------------- // 执行任务 - 1 setTimeout - 1 - setTimeout 添加了一个新的任务 - 3 - Promise 添加了一个新的微任务 // 执行任务 - 2 setTimeout - 2 - setTimeout 添加了一个新的任务 - 4 - Promise 添加了一个新的微任务 // timer 阶段结束 // 执行微任务 setTimeout - 1 - then - Promise 添加了一个新的微任务 // 执行微任务 setTimeout - 2 - then - Promise 添加了一个新的微任务 // 执行微任务 setTimeout - 1 - then - then // 执行微任务 setTimeout - 2 - then - then // 微任务处理完成 // 进入其余阶段开始循环 -> 直到再次进入 timer 阶段 // 执行任务 3 setTimeout - 1 - 1 // 执行任务 4 setTimeout - 2 - 1
正如以前所说 Node11 后执行逻辑发生了变化执行逻辑向浏览器靠拢, 这里给出 Node12 中执行相同代码的输出:
setTimeout - 1 setTimeout - 2 setTimeout - 1 - then setTimeout - 2 - then setTimeout - 1 - then - then setTimeout - 2 - then - then setTimeout - 1 - 1 setTimeout - 2 - 1
另外咱们都知道 Node 有一个 process.nextTick
, 修改以前的代码后:
setTimeout(() => { console.log('setTimeout - 1'); setTimeout(() => { console.log('setTimeout - 1 - 1') }); process.nextTick(() => { console.log('setTimeout - 1 - nextTick') process.nextTick(() => { console.log('setTimeout - 1 - nextTick - nextTick') }); }); }); setTimeout(() => { console.log('setTimeout - 2'); setTimeout(() => { console.log('setTimeout - 2 - 1') }); process.nextTick(() => { console.log('setTimeout - 2 - nextTick') process.nextTick(() => { console.log('setTimeout - 2 - nextTick - nextTick') }); }); });
在 Node10 中输出以下:
setTimeout - 1 setTimeout - 2 setTimeout - 1 - nextTick setTimeout - 2 - nextTick setTimeout - 1 - nextTick - nextTick setTimeout - 2 - nextTick - nextTick setTimeout - 1 - 1 setTimeout - 2 - 1
咱们能够看到输出和结果和使用 Promise
别无二致, 另外在 Node12 中的输出以下:
setTimeout - 1 setTimeout - 1 - nextTick setTimeout - 1 - nextTick - nextTick setTimeout - 2 setTimeout - 2 - nextTick setTimeout - 2 - nextTick - nextTick setTimeout - 1 - 1 setTimeout - 2 - 1
结果和以前的使用 Promise
也是没有区别的, 不过一样身为微任务源之一, 在 Node 中仍是有前后之分的:
setTimeout(() => { console.log('setTimeout - 1'); process.nextTick(() => { console.log('setTimeout - 1 - nextTick') process.nextTick(() => { console.log('setTimeout - 1 - nextTick - nextTick') }); }); new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 1 - then') new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 1 - then - then') }); }); });
Node10 和 Node12 输出:
setTimeout - 1 setTimeout - 1 - nextTick setTimeout - 1 - nextTick - nextTick setTimeout - 1 - then setTimeout - 1 - then - then
能够看出 process.nextTick
的优先级高于 Promise
的.
https://nodejs.org/en/docs/gu...https://nodejs.org/dist/lates...
https://www.dynatrace.com/new...
https://blog.csdn.net/Fundebu...
https://www.cnblogs.com/MuYun...
http://www.ruanyifeng.com/blo...
https://jsbin.com/dijodahawi/...