您现在的位置:首页 >> 前端 >> 内容

Node定时器实现详解

时间:2018/2/27 13:38:43 点击:

  核心提示:客户端异步实现我们经常在写客户端js的时候用到异步操作,实现异步基本上包含setTimeout setInterval ajaxNode端异步实现node中实现异步包含setImmediate pro...

客户端异步实现

我们经常在写客户端js的时候用到异步操作,实现异步基本上包含

setTimeout setInterval ajax

Node端异步实现

node中实现异步包含

setImmediate process.nextTick

定时器用法差不多

看下边的例子:

setTimeout(()=>{
    console.log(1);
});
setImmediate(()=>{
    console.log(2);
});
process.nextTick(()=>{
    console.log(3);
});
Promise.resolve().then(()=>{
    console.log(4);
});
(()=>console.log(5))();

输出结果如下:

5
3
4
1
2

下边来分析一下结果

同步和异步任务

同步任务要比异步任务更早执行,因此首先输出5

本轮循环和次轮循环

异步任务分为两种:

追加在本轮循环的异步任务 追加在次轮循环的异步任务

本轮循环任务要优先于次轮循环任务

Node规定,process.nextTick、Promise的回调函数,追加在本轮循环任务中,即同步任务完成后就开始执行。而setTimeout、setInterval、setImmediate的回调函数追加在次轮循环任务中。因此

// 下面两行,次轮循环执行
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
// 下面两行,本轮循环执行
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));

process.nextTick()

process.nextTick任务在本轮循环中执行,而且是所有异步任务里执行最快的。如果希望快速执行异步任务就使用此函数。

微任务

Promise对象的回调函数会进入异步任务里边的“微任务”队列,微任务队列追加在process.nextTick队列后边,也属于本轮循环。

Node定时器实现详解

注意:只有前一个队列全部清空后才会执行下一个队列。

看下边的例子:

process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 1
// 3
// 2
// 4

全部process.nextTick的回调函数会早于Promise

事件循环的概念

js只有一个主线程,事件循环是在主线程上完成的。

Node开始执行脚本时,先对事件循环进行初始化,但这时还没有执行事件循环,先进行一下操作。

同步任务 发出异步请求 规划定时器生效时间 执行本轮循环任务等等

上边事情都干完之后才进行事件循环。

事件循环的六个阶段

事件循环是无限次的执行,只有当异步任务回调函数队列都清空了,才会停止执行。

每一轮循环分为以下六个阶段

timers I/O callbacks idle,prepare poll check close callbacks

每个阶段都有一个先进先出的队列,只有该队列都清空或者该执行的回调函数都执行了,时间循环才进入下一个阶段。

Node定时器实现详解

(1) timers

这是一个定时器阶段,处理setTimeout,setInterval的回调函数,进入这个阶段,主线程会检查一下当前时间,是否满足定时器条件。如果满足就执行回调函数,否则调到下一个阶段。

(2) I/O callbacks

除了一下操作的回调函数,其他的回调函数都在这个阶段执行。

setTimeout()和setInterval()的回调函数 setImmediate()的回调函数 关于关闭请求的回调函数,例如socket.on(“close”,…)

本文中例子执行的是读取文件后的回调

(3) idle, prepare

该阶段只供 libuv 内部调用,这里可以忽略。

(4) Poll

这个阶段是轮询时间,用于等待未返回的I/O事件,比如服务器的回应,文件的读取,用户鼠标移动等等。

这个阶段的时间比较长,如果没有其他异步要处理(比如到期的定时器),会一直停留在这个阶段,等待I/O的响应。

(5) check

该阶段执行setImmediate()的回调函数。

(6) close callbacks

该阶段执行关闭请求的回调函数,比如socket.on(‘close’, …)。

Node定时器实现详解

事件循环的示例

分析一个官方案例:

const fs = require('fs');

const timeoutScheduled = Date.now();

// 异步任务一:100ms 后执行的定时器
setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`${delay}ms`);
}, 100);

// 异步任务二:文件读取后,有一个 200ms 的回调函数
fs.readFile('test.js', () => {
  const startCallback = Date.now();
  while (Date.now() - startCallback < 200) {
    // 什么也不做
  }
});

第一轮事件循环,没有到期的定时器,也没有可以进行的I/O回调操作,所以会键入Poll阶段,等待内核返回文件读取结果,由于文件过小,读取时间不会超过100ms,所以在定时器到期之前Poll阶段就会返回结果。继续往下执行。

第二轮事件循环,依然没有到期的定期器,因此会执行I/O的回调函数,进入I/O的callbacks阶段,执行fs.readFile的回调函数,这个函数需要200ms,也就是执行到一半,定时器就会到期,但是必须等到这个回调执行完之后才会离开这个阶段。

第三轮事件循环,已经有了到期的定时器,所以会在timers阶段执行定时器,最后输出大约200多毫秒。

setTimeout 和 setImmediate

由于setTimeout在timers阶段,setImmediate在check阶段,所以setTimeout会比setImmediate提前执行。

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));

上边代码应该先输出1后输出2,但是结果却是不确定的。有时会先输出2。

这是因为setTimeout的第二个参数默认为0,但是Node做不到0毫秒,最少也需要1ms,官方文档规定,setTimeout第二个参数范围在1~2147483647毫秒之间。

实际执行的时候,进入循环后,有可能到了1ms,也有可能没到1ms,取决于当前的系统状况。如果没到1ms那么timers阶段就会被跳过,进入check阶段,先执行setImmediate回调。

一下代码肯定是先输出2再输出1。

const fs = require('fs');

fs.readFile('test.js', () => {
  setTimeout(() => console.log(1));
  setImmediate(() => console.log(2));
});

Tags:NO OD DE E定 
作者:网络 来源:lianlin212