定时器
timer 用于安排函数在未来某个时间点被调用,Node.js 中的定时器函数实现了与 Web 浏览器提供的定时器 API 类似的 API,但是使用了事件循环实现,Node.js 中有四个相关的方法:
- setTimeout(callback, delay[, …args])
- setInterval(callback[, …args])
- setImmediate(callback[, …args])
- process.nextTick(callback[, …args])
// test.js
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
(() => console.log(5))();
异步任务可以分成两种。$ node test.js
5
3
4
1
2
- 追加在本轮循环的异步任务
- 追加在次轮循环的异步任务
所谓”循环”,指的是事件循环(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、setImmediate
setTimeout(()=>{
console.log('setTimeout')
}, 0)
setImmediate(()=>{
console.log('setImmediate')
})
//习题2: 都在回调函数中,内容确定
//首轮事件循环setTimeout1的timers清空,执行至check阶段,先输出setImmediate
//第二轮事件循环setTimeout2
//最终输出:setTimeout1、setImmediate、setTimeout2
setTimeout(()=>{
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.');