理解Node.js中的按时器
相关引荐:《node js教程》
timer 用于安排函数在将来某个时间点被调取,Node.js 中的按时器函数实现了与 Web 阅读器供给的按时器 API 相似的 API,但是使用了事件轮回实现,Node.js 中有四个相关的办法
setTimeout(callback, delay[, ...args])
setInterval(callback[, ...args])
setImmediate(callback[, ...args])
process.nextTick(callback[, ...args])
前两个含义和 web 上的是一致的,后两个是 Node.js 独占的,结果看起来就是 setTimeout(callback, 0),在 Node.js 编程中使用的最多
Node.js 不包管回调被触发确实切时间,也不包管它们的次序,回调会在尽大概接近指定的时间被调取。setTimeout 当 delay 大于 2147483647 或小于 1 时,则 delay 将会被设定为 1, 非整数的 delay 会被截断为整数
惊奇的施行次序
看一个示例,用几种办法离别异步打印一个数字
setImmediate(console.log, 1); setTimeout(console.log, 1, 2); Promise.resolve(3).then(console.log); process.nextTick(console.log, 4); console.log(5);
会打印 5 4 3 2 1 或者 5 4 3 1 2
同步 & 异步
第五行是同步施行,其它都是异步的
setImmediate(console.log, 1); setTimeout(console.log, 1, 2); Promise.resolve(3).then(console.log); process.nextTick(console.log, 4); /****************** 同步任务和异步任务的分割线 ********************/ console.log(5);
所以先打印 5,这个很好懂得,剩下的都是异步操纵,Node.js 依照什么次序施行呢?
event loop
Node.js 启动后会初始化事件轮询,历程中大概处置异步调取、按时器调度和 process.nextTick(),然后开端处置event loop。官网中有这样一张图用来介绍 event loop 操纵次序
┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘
event loop 的每个阶段都有一个任务队列,当 event loop 进入给定的阶段时,将施行该阶段的任务队列,直到队列清空或施行的回调到达系统上限后,才会转入下一个阶段,当所有阶段被次序施行一次后,称 event loop 完成了一个 tick
异步操纵都被放到了下一个 event loop tick 中,process.nextTick 在进入下一次 event loop tick 此前施行,所以必定在其它异步操纵此前
setImmediate(console.log, 1); setTimeout(console.log, 1, 2); Promise.resolve(3).then(console.log); /****************** 下次 event loop tick 分割线 ********************/ process.nextTick(console.log, 4); /****************** 同步任务和异步任务的分割线 ********************/ console.log(5);
各个阶段主要任务
timers:施行 setTimeout、setInterval 回调
pending callbacks:施行 I/O(文件、网络等) 回调
idle, prepare:仅供系统内部调取
poll:猎取新的 I/O 事件,施行相关回调,在恰当前提下把堵塞 node
check:setImmediate 回调在此阶段施行
close callbacks:施行 socket 等的 close 事件回调
日常开发中绝大部分异步任务都是在 timers、poll、check 阶段处置的
timers
Node.js 会在 timers 阶段检查可否有过期的 timer,假如存在则把回调放到 timer 队列中等候施行,Node.js 使用单线程,受限于主线程余暇状况和机器其它进程影响,并不克不及包管 timer 依照准确时间施行
按时器主要有两种
Immediate
Timeout
Immediate 类型的计时器回调会在 check 阶段被调取,Timeout 计时器会在设定的时间过期后尽快的调取回调,但
setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });
屡次施行会发明打印的次序不一样
poll
poll 阶段主要有两个任务
运算应当堵塞和轮询 I/O 的时间
然后,处置 poll 队列里的事件
当event loop进入 poll 阶段且没有被调度的计时器时
- 假如 poll 队列不是空的 ,event loop 将轮回拜访回调队列并同步施行,直到队列已用尽或者到达了系统或到达最大回调数
- 假如 poll 队列是空的
- 假如有 setImmediate() 任务,event loop 会在完毕 poll 阶段后进入 check 阶段
- 假如没有 setImmediate()任务,event loop 堵塞在 poll 阶段等候回调被增加到队列中,然后马上施行
一旦 poll 队列为空,event loop 将检查 timer 队列可否为空,假如非空则进入下一轮 event loop
上面提到了假如在不一样的 I/O 里,不克不及肯定 setTimeout 和 setImmediate 的施行次序,但假如 setTimeout 和 setImmediate 在一个 I/O 回调里,必定是 setImmediate 先施行,由于在 poll 阶段检查到有 setImmediate() 任务,event loop 直接进入 check 阶段施行 setImmediate 回调
const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
check
在该阶段施行 setImmediate 回调
为什么 Promise.then 比 setTimeout 早一些
前端同学必定都据说过 micoTask 和 macroTask,Promise.then 属于 microTask,在阅读器环境下 microTask 任务会在每个 macroTask 施行最末端调取
在 Node.js 环境下 microTask 会在每个阶段完成之间调取,也就是每个阶段施行最后都会施行一下 microTask 队列
setImmediate(console.log, 1); setTimeout(console.log, 1, 2); /****************** microTask 分割线 ********************/ Promise.resolve(3).then(console.log); // microTask 分割线 /****************** 下次 event loop tick 分割线 ********************/ process.nextTick(console.log, 4); /****************** 同步任务和异步任务的分割线 ********************/ console.log(5);
setImmediate VS process.nextTick
setImmediate 听起来是马上施行,process.nextTick 听起来是下一个时钟施行,为什么结果是反过来的?这就要从那段不胜回头的历史讲起
最开端的时候只要 process.nextTick 办法,没有 setImmediate 办法,通过上面的剖析可以看出来任何时候调取 process.nextTick(),nextTick 会在 event loop 此前施行,直到 nextTick 队列被清空才会进入到下一 event loop,假如显现 process.nextTick 的递归调取程序没有被准确完毕,那么 IO 的回调将没有时机被施行
const fs = require('fs'); fs.readFile('a.txt', (err, data) => { console.log('read file task done!'); }); let i = 0; function test(){ if(i++ < 999999) { console.log(`process.nextTick ${i}`); process.nextTick(test); } } test();
施行程序将返回
nextTick 1 nextTick 2 ... ... nextTick 999999 read file task done!
于是乎需要一个不这么 bug 的调取,setImmediate 办法显现了,比力令人费解的是在 process.nextTick 起错名字的状况下,setImmediate 也用了一个错误的名字以示区分。。。
那么是不是编程中应当杜绝使用 process.nextTick 呢?官方引荐大部分时候应当使用 setImmediate,同时对 process.nextTick 的最大调取堆栈做了限制,但 process.nextTick 的调取机制确实也能为我们解决一些棘手的问题
同意会员在 even tloop 开端此前 处置非常、施行清算任务
同意回调在调取栈 unwind 之后,下次 event loop 开端此前施行
一个类继承了 EventEmitter,并且想在实例化的时候触发一个事件
const EventEmitter = require('events'); const util = require('util'); function MyEmitter() { EventEmitter.call(this); this.emit('event'); } util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('an event occurred!'); });
在结构函数施行 this.emit('event')
会致使事件触发比事件回调函数绑定早,使用 process.nextTick 可以轻松实现预测结果
const EventEmitter = require('events'); const util = require('util'); function MyEmitter() { EventEmitter.call(this); // use nextTick to emit the event once a handler is assigned process.nextTick(() => { this.emit('event'); }); } util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('an event occurred!'); });