了解Node.js中的定时器


本文摘自PHP中文网,作者青灯夜游,侵删。

相关推荐:《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 会被截断为整数

奇怪的执行顺序

看一个示例,用几种方法分别异步打印一个数字

1

2

3

4

5

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

同步 & 异步

第五行是同步执行,其它都是异步的

1

2

3

4

5

6

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 操作顺序

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

┌───────────────────────────┐

┌─>│           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 之前执行,所以肯定在其它异步操作之前

1

2

3

4

5

6

7

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 计时器会在设定的时间过期后尽快的调用回调,但

1

2

3

4

5

6

7

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 回调

1

2

3

4

5

6

7

8

9

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 队列

1

2

3

4

5

6

7

8

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 的回调将没有机会被执行

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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();

执行程序将返回

1

2

3

4

5

6

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,而且想在实例化的时候触发一个事件

1

2

3

4

5

6

7

8

9

10

11

12

13

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 可以轻松实现预期效果

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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!');

});

更多编程相关知识,请访问:编程教学!!

以上就是了解Node.js中的定时器的详细内容,更多文章请关注木庄网络博客

相关阅读 >>

了解nodejs中的事件和事件循环

nodejs接口如何传输数据?

为什么要用node.js?哪些场合可以使用 node.js

h5的定时器怎样实现进度条功能

一起看看nodejs中的cookie和session

深入了解node.js中的非阻塞 i/o

详解node.js中的quic协议

如何让定时器刷新页面

详解javascript是如何运行的

深入了解node.js中的koa框架

更多相关阅读请进入《node.js》频道 >>




打赏

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,您说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

分享从这里开始,精彩与您同在

评论

管理员已关闭评论功能...