极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node

本文更佳阅读体验:https://www.yuque.com/sunluyong/node/timer

timer 用于安排函数在未来某个时间点被调用,Node.js 中的定时器函数实现了与 Web 浏览器提供的定时器 API 类似的 API,但是使用了事件循环实现,Node.js 中有四个相关的方法

  1. setTimeout(callback, delay[, ...args])
  2. setInterval(callback[, ...args])
  3. setImmediate(callback[, ...args])
  4. 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);

各个阶段主要任务

  1. timers:执行 setTimeout、setInterval 回调
  2. pending callbacks:执行 I/O(文件、网络等) 回调
  3. idle, prepare:仅供系统内部调用
  4. poll:获取新的 I/O 事件,执行相关回调,在适当条件下把阻塞 node
  5. check:setImmediate 回调在此阶段执行
  6. close callbacks:执行 socket 等的 close 事件回调

日常开发中绝大部分异步任务都是在 timers、poll、check 阶段处理的

timers

Node.js 会在 timers 阶段检查是否有过期的 timer,如果存在则把回调放到 timer 队列中等待执行,Node.js 使用单线程,受限于主线程空闲情况和机器其它进程影响,并不能保证 timer 按照精确时间执行
定时器主要有两种

  1. Immediate
  2. Timeout

Immediate 类型的计时器回调会在 check 阶段被调用,Timeout 计时器会在设定的时间过期后尽快的调用回调,但

setTimeout(() => {
console.log('timeout');
}, 0); setImmediate(() => {
console.log('immediate');
});

多次执行会发现打印的顺序不一样

poll

poll 阶段主要有两个任务

  1. 计算应该阻塞和轮询 I/O 的时间
  2. 然后,处理 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 的调用机制确实也能为我们解决一些棘手的问题

  1. 允许用户在 even tloop 开始之前 处理异常、执行清理任务
  2. 允许回调在调用栈 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!');
});

参考

  1. Node.js 事件循环,定时器和 process.nextTick()
  2. 深入理解js事件循环机制(Node.js篇)

极简 Node.js 入门 - 2.4 定时器的更多相关文章

  1. 极简 Node.js 入门 - 4.3 可读流

    极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...

  2. 极简 Node.js 入门 - Node.js 是什么、性能有优势?

    极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...

  3. 极简 Node.js 入门 - 1.2 模块系统

    极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...

  4. 极简 Node.js 入门 - 1.3 调试

    极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...

  5. 极简 Node.js 入门 - 1.4 NPM & package.json

    极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...

  6. 极简 Node.js 入门 - 2.1 Path

    极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...

  7. 极简 Node.js 入门 - 2.2 事件

    极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...

  8. 极简 Node.js 入门 - 2.3 process

    极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...

  9. 极简 Node.js 入门 - 3.1 File System API 风格

    极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...

随机推荐

  1. 【揭秘】C语言类型转换时发生了什么?

    ID:技术让梦想更伟大 作者:李肖遥 链接:https://mp.weixin.qq.com/s/ZFf3imVaJgeesuhl1Kn9sQ 在C语言中,数据类型指的是用于声明不同类型的变量或函数的 ...

  2. abp vnext 开发快速入门 3 实现权限控制

    上篇讲了abp vnext 实现了简单的增加操作的例子.删除更新查询基本类似,这里就不讲了,接下来说下如何实现角色权限控制. 再说之前,先说下如果想更加透彻的理解abp vnext的权限控制,最好是先 ...

  3. Python 实现将numpy中的nan和inf,nan替换成对应的均值

    nan:not a number inf:infinity;正无穷 numpy中的nan和inf都是float类型     t!=t 返回bool类型的数组(矩阵) np.count_nonzero( ...

  4. 各版本arm-gcc区别与安装【转】

    转自:https://www.jianshu.com/p/fd0103d59d8e arm-linux-gcc.arm-none-eabi-gcc.arm-eabi-gcc.arm-none-linu ...

  5. Python while 循环中使用 else 语句

    Python while 循环中使用 else 语句: else:表示 while 中的语句正常执行完,然后执行 else 语句的部分. 示例: # while 判断条件: # 一行语句 或 多行语句 ...

  6. PHP tmpfile() 函数

    定义和用法 tmpfile() 函数以读写(w+)模式创建一个具有唯一文件名的临时文件. 语法 tmpfile() 提示和注释 注释:临时文件会在文件关闭后(用 fclose())或当脚本结束后自动被 ...

  7. PHP atan2() 函数

    实例 通过 atan2() 函数返回两个变量的反正切: <?phpecho(atan2(0.50,0.50) . "<br>");echo(atan2(-0.50 ...

  8. PDOStatement::fetch

    PDOStatement::fetch — 从结果集中获取下一行(PHP 5 >= 5.1.0, PECL pdo >= 0.1.0) 说明 语法 mixed PDOStatement:: ...

  9. 初学编程丨从零开始学习编程的基本路线,BAT程序员亲手总结!

    编程并不是说代码怎么写,框架怎么用,业务怎么转换为代码逻辑,这些都不是编程的要素(但却是工作的刚需......).我认为按照下面这个路线来学习编程,会使自己在学习的路途上少去很多问题(比如为啥会有多线 ...

  10. java -jar .jar中没有主清单属性

    pom里加上 <build> <plugins> <plugin> <groupId>org.springframework.boot</grou ...