解读setTimeout, promise.then, process.nextTick, setImmediate的执行顺序
最近在看《Node.js调试指南》的时候遇到有意思的几道题,是关于setTimeout, promise.then, process.nextTick, setImmediate的执行顺序。今天抽空记录下这道题的分析过程及背后的原理与知识点。
题目如下:
// 题目一:
setTimeout(()=>{
console.log('setTimeout')
},0)
setImmediate(()=>{
console.log('setImmediate')
})
// 题目二:
const promise = Promise.resolve()
promise.then(()=>{
console.log('promise')
})
process.nextTick(()=>{
console.log('nextTick')
})
// 题目三:
setTimeout (() => {
console.log(1)
},0)
new Promise((resolve,reject) => {
console.log(2)
for(let i = 0; i <10000; i++) {
i === 9999 && resolve()
}
console.log(3)
}).then(() => {
console.log(4)
})
console.log(5)
// 题目四
setInterval(()=>{
console.log('setInterval')
},100)
process.nextTick(function tick(){
process.nextTick(tick)
})
在分析这几道题之前先有必要了解下node.js的事件循环
事件循环 Event Loop
我们可以简单理解Event Loop如下:
- 所有任务都在主线程上执行,形成一个执行栈(Execution Context Stack)
- 在主线程之外还存在一个任务队列(Task Queen),系统把异步任务放到任务队列中,然后主线程继续执行后续的任务
- 一旦执行栈中所有的任务执行完毕,系统就会读取任务队列。如果这时异步任务已结束等待状态,就会从任务队列进入执行栈,恢复执行
- 主线程不断重复上面的第三步
上面第三步中的读取任务队列包括以下6个阶段

- timers:执行setTimeout()和setInterval()中到期的callback
- I/O callbacks:上一轮循环中有少数的I/O callback会被延迟到这一轮的这一阶段
- idle,prepare:仅内部调用
- poll:最重要的阶段,执行I/O callback,在某些条件下node会阻塞在这个阶段
- check:执行setImmediate()的callback
- close callbacks:执行close事件的callback,例如socket.on('close',func)
每个阶段都有一个FIFO的回调队列,当Event Loop执行到这个阶段时,就会从当前阶段的队列里拿出一个任务放到执行栈中执行,在队列任务清空或者执行的回调数量达到上限后,Event Loop就会进入下一个阶段
poll阶段
poll阶段主要有两个功能,如下所述:
- 当timers的定时器到期后,执行定时器(setTimeout和setInterval)的callback
- 执行poll队列里面的I/O callback
如果Event Loop进入了poll阶段,且代码未设定timer,则可能发生以下的情况:
- 如果poll queue不为空,则Event Loop将同步执行queue里的callback,直至queue为空,或者执行的callback达到系统上限
- 如果poll queue为空,则可能发生以下情况:
- 如果代码中使用了setImmediate(),则Event Loop将结束poll阶段并进入check阶段,执行check阶段的代码
- 如果代码中没有使用setImmediate(),则Event Loop将阻塞在该阶段,等待callback加入poll queue,如果有callback进来则立即执行
一旦poll queue为空,则Event Loop将检查timers,如果有timer的时间到期,则Event Loop将回到timers阶段,然后执行timer queue
事件循环原理
- node 的初始化
- 初始化 node 环境。
- 执行输入代码。
- 执行 process.nextTick 回调。
- 执行 microtasks。
- 进入 event-loop
进入 timers 阶段
- 检查 timer 队列是否有到期的 timer 回调,如果有,将到期的 timer 回调按照 timerId 升序执行。
- 检查是否有 process.nextTick 任务,如果有,全部执行。
- 检查是否有microtask,如果有,全部执行。
- 退出该阶段。
进入IO callbacks阶段。
- 检查是否有 pending 的 I/O 回调。如果有,执行回调。如果没有,退出该阶段。
- 检查是否有 process.nextTick 任务,如果有,全部执行。
- 检查是否有microtask,如果有,全部执行。
- 退出该阶段。
进入 idle,prepare 阶段:
- 这两个阶段与我们编程关系不大,暂且按下不表。
进入 poll 阶段
- 首先检查是否存在尚未完成的回调,如果存在,那么分两种情况。
- 第一种情况:
- 如果有可用回调(可用回调包含到期的定时器还有一些IO事件等),执行所有可用回调。
- 检查是否有 process.nextTick 回调,如果有,全部执行。
- 检查是否有 microtaks,如果有,全部执行。
- 退出该阶段。
- 第二种情况:
- 如果没有可用回调。
- 检查是否有 immediate 回调,如果有,退出 poll 阶段。如果没有,阻塞在此阶段,等待新的事件通知。
- 第一种情况:
- 如果不存在尚未完成的回调,退出poll阶段。
- 首先检查是否存在尚未完成的回调,如果存在,那么分两种情况。
进入 check 阶段。
- 如果有immediate回调,则执行所有immediate回调。
- 检查是否有 process.nextTick 回调,如果有,全部执行。
- 检查是否有 microtaks,如果有,全部执行。
- 退出 check 阶段
进入 closing 阶段。
- 如果有immediate回调,则执行所有immediate回调。
- 检查是否有 process.nextTick 回调,如果有,全部执行。
- 检查是否有 microtaks,如果有,全部执行。
- 退出 closing 阶段
检查是否有活跃的 handles(定时器、IO等事件句柄)。
- 如果有,继续下一轮循环。
- 如果没有,结束事件循环,退出程序。
通过上面的事件循环的介绍我们已经知道setTimeout setImmediate的执行机制,但是并没有介绍process.nextTick()和promise.then()。这里我们还需要知道宏任务与微任务的概念
宏任务 Macrotask
宏任务是指Event Loop在每个阶段执行的任务
宏任务包括 script (整体代码),setTimeout, setInterval, setImmediate, I/O, UI renderin
微任务 Microtask
微任务是指Event Loop在每个阶段之间执行的任务
微任务包括 process.nextTick, Promise.then,Object.observe,MutationObserver
宏任务与微任务执行顺序图

图中绿色小块表示Event Loop的各个阶段,执行的是宏任务,粉色箭头表示执行的是微任务
了解到这里我们再来分析上面的几道题
题目一的执行结果是:
setTimeout
setImmediate
//或者
setImmediate
setTimeout
为什么结果不确定呢?我们知道setTimeout的回调函数在timer阶段执行,setImmediate的回调函数在check阶段执行。但是从事件循环开始到timer阶段会消耗一定的时间,所以会出现两种情况:
- 若timer前的准备时间超过1ms,则执行timer阶段(setTimeout)的回调函数
- 若timer前的准备时间少于1ms,则执行check阶段(setImmediate)的回调函数,下次event loop循环在执行timer阶段的函数
题目二的执行结果是
nextTick
promise
这里虽然和process.nextTick一样,promise.then也将回调函数注册到microtask,但process.nextTick的microtask queue总是优先于promise的microtask queue执行的
题目三的执行结果是
2
3
5
4
1
Promise构造函数是同步执行的,所以先打印2,3,在打印5,接下来事件循环执行微任务执行promise.then的回调,打印4,然后进入下一个事件循环执行timer阶段的回调打印1
题目四的执行结果是
永远不会打印setInterval
process.nextTick会无限循环,将event loop阻塞在microtask阶段,导致event loop上其他macrotask阶段的回调函数没有机会执行
解决方法通常是用setImmediate代替process.nextTick.
在setImmediate内执行setImmedaite时会将immediate注册到下一次event loop的check阶段,这样其他macrotask就有机会执行
至此终于将node.js事件循环宏任务与微任务分析清楚了
解读setTimeout, promise.then, process.nextTick, setImmediate的执行顺序的更多相关文章
- js的事件循环机制:同步与异步任务(setTimeout,setInterval)宏任务,微任务(Promise,process.nextTick)
javascript是单线程,一切javascript版的"多线程"都是用单线程模拟出来的,通过事件循环(event loop)实现的异步. javascript事件循环 事件循环 ...
- 详解promise、async和await的执行顺序
1.题目和答案 一道题题目:下面这段promise.async和await代码,请问控制台打印的顺序? async function async1(){ console.log('async1 sta ...
- promise、async和await之执行顺序
async function async1(){ console.log('async1 start') await async2() console.log('async1 end') } asyn ...
- promise、async、await、settimeout异步原理与执行顺序
一道经典的前端笔试题,你能一眼写出他们的执行结果吗? async function async1() { console.log("async1 start"); await as ...
- [NodeJs系列][译]理解NodeJs中的Event Loop、Timers以及process.nextTick()
译者注: 为什么要翻译?其实在翻译这篇文章前,笔者有Google了一下中文翻译,看的不是很明白,所以才有自己翻译的打算,当然能力有限,文中或有错漏,欢迎指正. 文末会有几个小问题,大家不妨一起思考一下 ...
- The Node.js Event Loop, Timers, and process.nextTick() Node.js事件循环,定时器和process.nextTick()
个人翻译 原文:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ The Node.js Event Loop, Ti ...
- 关于async/await、promise和setTimeout执行顺序
先来一道关于async/await.promise和setTimeout的执行顺序的题目: async function async1() { console.log('async1 start'); ...
- promise 和 setTimeout 在任务队列的执行顺序
setTimeout(() => { console.log() }); const a = new Promise((resolve,reject)=>{ console.log(); ...
- 宏任务和微任务:setTimeout和Promise执行顺序
先以一道面试题做引子: 写出这段程序的输出内容: setTimeout(function(){ console.log(); },); new Promise(function(a,b){ conso ...
随机推荐
- 官方宣布IPV4已然耗尽,IPv6D风口或将到来?
急救箱 IPV4真的用完了吗? IPV4真的用完了吗?其实 小兰 一点也不惊讶 ,毕竟全球人口这么多,多N的几次幂就用完了吧- 43亿个IPv4地址已分配完毕,这意味着没已经有更多的IPv4地址可 ...
- windows系统与SQL SERVER 2008数据库服务性能监控分析简要
软件系统性能测试体系流程介绍之windows系统与SQL SERVER 2008数据库服务性能监控分析简要 目前大部分测试人员对操作系统资源.中间件.数据库等性能监控分析都是各自分析各自的监控指标方式 ...
- NN入门,手把手教你用Numpy手撕NN(三)
NN入门,手把手教你用Numpy手撕NN(3) 这是一篇包含极少数学的CNN入门文章 上篇文章中简单介绍了NN的反向传播,并利用反向传播实现了一个简单的NN,在这篇文章中将介绍一下CNN. CNN C ...
- 一篇文章搞定Python多进程(全)
1.Python多进程模块 Python中的多进程是通过multiprocessing包来实现的,和多线程的threading.Thread差不多,它可以利用multiprocessing.Proce ...
- mybatis源码学习(一) 原生mybatis源码学习
最近这一周,主要在学习mybatis相关的源码,所以记录一下吧,算是一点学习心得 个人觉得,mybatis的源码,大致可以分为两部分,一是原生的mybatis,二是和spring整合之后的mybati ...
- 69道Spring面试题及答案
目录 Spring 概述 依赖注入 Spring beans Spring注解 Spring数据访问 Spring面向切面编程(AOP) Spring MVC Spring 概述 1. 什么是spri ...
- Java基础面试题及答案(一)
Java 基础部分 1. JDK 和 JRE 有什么区别? JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境. JRE:Java ...
- oracle 触发器(自增写法)
触发器trigger 触发器我们也可以认为是存储过程,是一种特殊的存储过程. 存储过程:有输入参数和输出参数,定义之后需要调用 触发器:没有输入参数和输出参数,定义之后无需调用,在适当的时候会自动执行 ...
- 如何使用JavaScript直接上传并预览粘贴板的图片?
(题图:梵高-橄榄树) 提出需求 因为工作原因,现在有一个需求就是需要用户使用QQ或者微信复制一张截图后,在div中直接粘贴这张图片,而不是采用上传的方式.类似我们在使用QQ微信时直接粘贴截图的操作, ...
- 使用HttpReports快速搭建API分析平台
HttpReports 简单介绍 HttpReports 是 .Net Core下的一个Web组件,适用于 WebAPI 项目和 API 网关项目,通过中间件的形式集成到您的项目中, 通过HttpRe ...