定时器
timer 用于安排函数在未来某个时间点被调用,Node.js 中的定时器函数实现了与 Web 浏览器提供的定时器 API 类似的 API,但是使用了事件循环实现,Node.js 中有四个相关的方法:
- setTimeout(callback, delay[, …args])
- setInterval(callback[, …args])
- setImmediate(callback[, …args])
- process.nextTick(callback[, …args])
// test.jssetTimeout(() => console.log(1));setImmediate(() => console.log(2));process.nextTick(() => console.log(3));Promise.resolve().then(() => console.log(4));(() => console.log(5))();
异步任务可以分成两种。$ node test.js53412
- 追加在本轮循环的异步任务
- 追加在次轮循环的异步任务
所谓”循环”,指的是事件循环(event loop)。这是 JavaScript 引擎处理异步任务的方式,
Node 规定,process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。
事件循环
┌───────────────────────────┐┌─>│ timers │ setTimeout/setImediate│ └─────────────┬─────────────┘│ ┌─────────────┴─────────────┐│ │ pending callbacks │ --│ └─────────────┬─────────────┘│ ┌─────────────┴─────────────┐│ │ idle, prepare │ --│ └─────────────┬─────────────┘ ┌───────────────┐│ ┌─────────────┴─────────────┐ │ incoming: ││ │ poll │<─────┤ connections, │ 轮询│ └─────────────┬─────────────┘ │ data, etc. ││ ┌─────────────┴─────────────┐ └───────────────┘│ │ check │ - setImmdiate│ └─────────────┬─────────────┘│ ┌─────────────┴─────────────┐└──┤ close callbacks │ - 关闭回调└───────────────────────────┘
注意:每个框被称为事件循环机制的⼀个阶段。 每个阶段都有⼀个 FIFO 队列来执行回调。
node 开始执行脚本时,会先进行事件循环的初始化,但是这时事件循环还没有开始,会先完成下面的事情。
- 同步任务
- 发出异步请求
- 规划定时器生效的时间
- 执行微任务 process.nextTick() 等
然后进入事件循环
- timers:此阶段执行由 setTimeout 和 setInterval 设置的回调。
pending callbacks:执行推迟到下一个循环迭代的 I/O 回调。
此阶段对某些系统操作(如 TCP 错误类型)执行回调。例如,如果 TCP 套接字在尝试连接时接收到 ECONNREFUSED,则某些 *nix 的系统希望等待报告错误。这将被排队以在 挂起的回调 阶段执行。
idle, prepare, :仅在内部使用,可以忽略。
poll:轮询
轮询阶段有两个重要的功能:
- 计算应该阻塞和轮询 I/O 的时间。
- 然后处理轮询队列里的事件。
check:在这里调用 setImmediate 回调。
- close callbacks:一些关闭回调,例如 socket.on(‘close’, …)。
微任务在各个阶段之间执行
setImmediate
setImmediate() 和 setTimeout() 很类似,但是基于被调用的时机,他们也有不同表现。
- setImmediate() 设计为一旦在当前 轮询 阶段完成, 就执行脚本。
- setTimeout() 在最小阈值(ms 单位)过后运行脚本。
- setTimeout插入的时机不一定,有一定的延时
- 如果运行以下不在 I/O 周期(即主模块)内的脚本,则执行两个计时器的顺序是非确定性的
- 一个 I/O 循环内调用,setImmediate 总是被优先调用。
// 顺序不确定,只有两个语句,执行环境有差异// 场景1: setTimeout 0 最少1ms,未推入timers队列中,执行结果为:setImmediate、setTimeout// 场景2: setTimeout 0 已推入timers队列中,执行结果为:setTimeout、setImmediatesetTimeout(()=>{console.log('setTimeout')}, 0)setImmediate(()=>{console.log('setImmediate')})//习题2: 都在回调函数中,内容确定//首轮事件循环setTimeout1的timers清空,执行至check阶段,先输出setImmediate//第二轮事件循环setTimeout2//最终输出:setTimeout1、setImmediate、setTimeout2setTimeout(()=>{setTimeout(()=>{console.log('setTimeout2')}, 0)setImmediate(()=>{console.log('setImmediate')})console.log('setTimeout1')}, 0)
微任务调整
- node11 开始,每执行完一个timer类回调,例如 setTimeout, setImmediate 之后,都会把微任务给执行掉(promise等)
- node10和以前: 当一个任务队列(例如timer queue)里面的回调都批量执行完了,才去执行微任务
async function async1(){console.log('async1 started');await async2();console.log('async end');}async function async2(){console.log('async2');}console.log('script start.');setTimeout(() => {console.log('setTimeout0');setTimeout(() => {console.log('setTimeout1');}, 0);setImmediate(()=>{console.log('setImmediate');})}, 0);process.nextTick(() => {console.log('nextTick');})async1();new Promise(()=>{console.log('promise1');resolve();console.log('promise2');}).then(() =>{console.log('promise.then')});console.log('script end.');
