setTimeout、setImmediate、process.nextTick、promise.then 的区别

3,239 阅读4分钟

最近在学习node 方面的知识,碰到一个题目,挺有意义,在此记录一下。这也是在掘金的第一篇文章。

console.time('start');

setTimeout(function() {
  console.log(2);
}, 10);

setImmediate(function() {
  console.log(1);
});

new Promise(function(resolve) {
  console.log(3);
  resolve();
  console.log(4);
}).then(function() {
  console.log(5);
  console.timeEnd('start')
});

console.log(6);

process.nextTick(function() {
  console.log(7);
});

console.log(8);

下面我们带着问题去思考,关于setTimeout、setImmediate、process.nextTick、promise.then 的执行顺序。那接下来我们就一起分析一下这道题背后的原理和知识点。

在分析这道题之前,首先要搞清楚 setImmediate、process.nextTick 是干什么的。这里就涉及到了 node.js 的相关知识。

node 是什么

node是javascript运行环境,就像浏览器一样,是一个平台。在浏览器中,V8引擎负责解释javascript,你在javascript调用的接口都是浏览器实现并提供的,浏览器会调用底层的、由其他语言(C++)实现并封装好的接口来完成任务;同样,在node中,也是V8引擎负责解释javascript。详细了解请前往》

setImmediate、process.nextTick 是什么

为了协调异步任务,Node 提供了四个定时器,让任务可以在指定的时间运行

  • setTimeout()
  • setInterval()
  • setImmediate()
  • process.nextTick()

前两个是语言的标准,后两个是 Node 独有的 详细了解请前往》

node.js 核心特性

  • 单线程
  • 非阻塞I/O
  • 事件驱动
    • 浏览器端把鼠标点击、键盘按键等定义为事件,而node把网络请求、I/O操作等也看作事件,严格来说,一切动作都是事件,这就是事件驱动的思想
    • 在程序启动时,便进入事件循环,不断遍历执行事件队列中产生的事件,而在执行过程中,又会产生新的事件,因此称为事件循环

题目解析

我们上面的题目,考察的就是 node 事件循环 Event Loop 我们可以简单理解Event Loop如下:

  1. 所有任务都在主线程上执行,形成一个执行栈(Execution Context Stack)
  2. 在主线程之外还存在一个任务队列(Task Queen),系统把异步任务放到任务队列中,然后主线程继续执行后续的任务
  3. 一旦执行栈中所有的任务执行完毕,系统就会读取任务队列。如果这时异步任务已结束等待状态,就会从任务队列进入执行栈,恢复执行
  4. 主线程不断重复上面的第三步

在上述的例子中,我们明白首先执行主线程中的同步任务,因此依次输出3、4、6、8。当主线程任务执行完毕后,再从Event Loop中读取任务。

Event Loop读取任务的先后顺序,取决于任务队列(Job queue)中对于不同任务读取规则的限定。

Job queue中的执行顺序

在Job queue中的队列分为两种类型:

宏任务 Macrotask 宏任务是指Event Loop在每个阶段执行的任务

微任务 Microtask 微任务是指Event Loop在每个阶段之间执行的任务

我们举例来看执行顺序的规定,我们假设

宏任务队列包含任务: A1, A2 , A3

微任务队列包含任务: B1, B2 , B3

执行顺序为,首先执行宏任务队列开头的任务,也就是 A1 任务,执行完毕后,在执行微任务队列里的所有任务,也就是依次执行B1, B2 , B3,执行完后清空微任务队中的任务,接着执行宏任务中的第二个任务A2,依次循环。

了解完了宏任务 Macrotask微任务 Microtask两种队列的执行顺序之后,我们接着来看,真实场景下这两种类型的队列里真正包含的任务(我们以node V8引擎为例),在node V8中,这两种类型的真实任务顺序如下所示:

宏任务 Macrotask队列真实包含任务:

script(主程序代码),setTimeout, setInterval, setImmediate, I/O, UI rendering

微任务 Microtask队列真实包含任务:

process.nextTick, Promises, Object.observe, MutationObserver

由此我们得到的执行顺序应该为:

script(主程序代码)—>process.nextTick—>Promises...——>setTimeout——>setInterval——>setImmediate——> I/O——>UI rendering

在ES6中宏任务 Macrotask队列又称为ScriptJobs,而微任务 Microtask又称PromiseJobs

我们的题目相对复杂,但是要注意,我们在定义promise的时候,promise构造部分是同步执行的

接下来我们分析我们的题目,首先分析Job queue的执行顺序:

script(主程序代码)——>process.nextTick——>promise——>setTimeout——>setImmediate

  • 主体部分: 定义promise的构造部分是同步的,因此先输出3、4 ,主体部分再输出6、8(同步情况下,就是严格按照定义的先后顺序)
  • process.nextTick: 输出7
  • promise: 这里的promise部分,严格的说其实是promise.then部分,输出的是5、以及 timeEnd('start')
  • setImmediate:输出1,依据上面优先级,应该先setTimeout,但是注意,setTimeout 设置 10ms 延时
  • setTimeout : 输出2

综合的执行顺序就是: 3——>4——>6——>8——>7——>5——>start: 7.009ms——>1——>2

总结

此次主要从题目入手,理解setTimeout、setImmediate、process.nextTick、promise.then 的区别。关于node 方面的知识没有详细说明(正在学习中...)后续准备将自己的学习的知识进行总结分享,加深印象。