极简 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. java基础知识--环境变量配置

    说到java,大家应该都了解:J2SE基础.J2ME嵌入式.J2EE延伸的内容,而我们平时接触和所谈大部分都是J2EE企业级应用开发. 作为开发者,使用java之前,必须安装java开发环境,配置ja ...

  2. Zookeeper ----- ZAB算法

    介绍 Zookeeper没有使用Paxos实现,而是使用ZAB(Zookeeper原子消息广播协议)作为数据一致性的核心算法. ZAB是一种专为Zookeeper设计的支持崩溃恢复的原子广播协议. Z ...

  3. python学习之路------你想要的都在这里了

    python学习之路------你想要的都在这里了 (根据自己的学习进度后期不断更新哟!!!) 一.python基础 1.python基础--python基本知识.七大数据类型等 2.python基础 ...

  4. 利用Data vault对数据仓库建模

    简介 国内关于Data Vault的信息很少,所以决定写点什么,纯粹都是自己在这个行业10多年的摸爬滚打.不过为了效率,尽量做到简短,直接上干货.对于各个细节大家有不同的理解欢迎来讨论. 数据仓库建模 ...

  5. Ansible基础

    Ansible基于Python paramiko 开发,分布式,无需客户端,轻量级,配置语法使用YMAL 及Jinja2模板语言. 组件: 核心:ansible 核心模块(Core Modules): ...

  6. Socket通信,基本方法介绍

    Socket是什么呢? Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口. 在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口 ...

  7. php imap 那些坑

    今天调试php 接收邮件,遇见的几大坑! 第一,返回错误 关键字imap_open返回flase 原来{{$mailServer}:143}INBOX  的端口,不是根据outlook给的,,,,,是 ...

  8. 关闭,centos yum的自动更新

    今天我虚拟机提示我满了,,,打开磁盘分析器一看,yum的更新目录满了,本地虚拟跟生产环境的版本完全一致,所以我也不打算更新,找一下命令,得先安装yum-cron ,然后再禁止更新 [root@loca ...

  9. 关于if后有无else谜题

    在自定义函数中 for(i=1;i<=6;i++) { if(a%i==0) return 0; else return 1; } 意思是,如果有一个除不尽就返回零,有一个除尽会立刻返回一. 如 ...

  10. 安排上了!PC人脸识别登录,出乎意料的简单

    本文收录在个人博客:www.chengxy-nds.top,技术资源共享. 之前不是做了个开源项目嘛,在做完GitHub登录后,想着再显得有逼格一点,说要再加个人脸识别登录,就我这佛系的开发进度,过了 ...