VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > temp > JavaScript教程 >
  • 【THE LAST TIME】彻底吃透 JavaScript 执行机制(2)

 和 setInterval 回调,并且是由 poll 阶段控制的。

在 timers 阶段其实使用一个最小堆而不是队列来保存所有的元素,其实也可以理解,因为timeout的callback是按照超时时间的顺序来调用的,并不是先进先出的队列逻辑)。而为什么 timer 阶段在第一个执行阶梯上其实也不难理解。在 Node 中定时器指定的时间也是不准确的,而这样,就能尽可能的准确了,让其回调函数尽快执行。

以下是官网给出的例子:

const fs = require('fs');

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;

  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);

// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
  const startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});

当进入事件循环时,它有一个空队列(fs.readFile()尚未完成),因此定时器将等待剩余毫秒数,当到达95ms时,fs.readFile()完成读取文件并且其完成需要10毫秒的回调被添加到轮询队列并执行。

当回调结束时,队列中不再有回调,因此事件循环将看到已达到最快定时器的阈值,然后回到timers阶段以执行定时器的回调。
在此示例中,您将看到正在调度的计时器与正在执行的回调之间的总延迟将为105毫秒。

pending callbacks 阶段

pending callbacks 阶段其实是 I/O 的 callbacks 阶段。比如一些 TCP 的 error 回调等。

举个栗子:如果TCP socket ECONNREFUSED在尝试connectreceives,则某些* nix系统希望等待报告错误。 这将在pending callbacks阶段执行。

poll 阶段

poll 阶段主要有两个功能:

  • 执行 I/O 回调
  • 处理 poll 队列(poll queue)中的事件

当时Event Loop 进入到 poll 阶段并且 timers 阶段没有任何可执行的 task 的时候(也就是没有定时器回调),将会有以下两种情况

  • 如果 poll queue 非空,则 Event Loop就会执行他们,知道为空或者达到system-dependent(系统相关限制)
  • 如果 poll queue 为空,则会发生以下一种情况
    • 如果setImmediate()有回调需要执行,则会立即进入到 check 阶段
    • 相反,如果没有setImmediate()需要执行,则 poll 阶段将等待 callback 被添加到队列中再立即执行,这也是为什么我们说 poll 阶段可能会阻塞的原因。

一旦 poll queue 为空,Event Loop就回去检查timer 阶段的任务。如果有的话,则会回到 timer 阶段执行回调。

check 阶段

check 阶段在 poll 阶段之后,setImmediate()的回调会被加入check队列中,他是一个使用libuv API 的特殊的计数器。

通常在代码执行的时候,Event Loop 最终会到达 poll 阶段,然后等待传入的链接或者请求等,但是如果已经指定了setImmediate()并且这时候 poll 阶段已经空闲的时候,则 poll 阶段将会被中止然后开始 check 阶段的执行。

close callbacks 阶段

如果一个 socket 或者事件处理函数突然关闭/中断(比如:socket.destroy()),则这个阶段就会发生 close 的回调执行。否则他会通过 process.nextTick() 发出。

setImmediate() vs setTimeout()

setImmediate() 和 setTimeout()非常的相似,区别取决于谁调用了它。

  • setImmediate在 poll 阶段后执行,即check 阶段
  • setTimeout 在 poll 空闲时且设定时间到达的时候执行,在 timer 阶段

计时器的执行顺序将根据调用它们的上下文而有所不同。 如果两者都是从主模块中调用的,则时序将受到进程性能的限制。

例如,如果我们运行以下不在I / O周期(即主模块)内的脚本,则两个计时器的执行顺序是不确定的,因为它受进程性能的约束:

// timeout_vs_immediate.js
setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

如果在一个I/O 周期内移动这两个调用,则始终首先执行立即回调:

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout

所以与setTimeout()相比,使用setImmediate()的主要优点是,如果在I / O周期内安排了任何计时器,则setImmediate()将始终在任何计时器之前执行,而与存在多少计时器无关。

nextTick queue

可能你已经注意到process.nextTick()并未显示在图中,即使它是异步API的一部分。 所以他拥有一个自己的队列:nextTickQueue

这是因为process.nextTick()从技术上讲不是Event Loop的一部分。 相反,无论当前事件循环的当前阶段如何,都将在当前操作完成之后处理nextTickQueue

如果存在 nextTickQueue,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

setTimeout(() => {
 console.log('timer1')
 Promise.resolve().then(function() {
   console.log('promise1')
 })
}, 0)
process.nextTick(() => {
 console.log('nextTick')
 process.nextTick(() => {
   console.log('nextTick')
   process.nextTick(() => {
     console.log('nextTick')
     process.nextTick(() => {
       console.log('nextTick')
     })
   })
 })
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1

process.nextTick() vs setImmediate()

从使用者角度而言,这两个名称非常的容易让人感觉到困惑。

  • process.nextTick()在同一阶段立即触发
  • setImmediate()在事件循环的以下迭代或“tick”中触发

貌似这两个名称应该呼唤下!的确~官方也这么认为。但是他们说这是历史包袱,已经不会更改了。

这里还是建议大家尽可能使用setImmediate。因为更加的让程序可控容易推理。

至于为什么还是需要 process.nextTick,存在即合理。这里建议大家阅读官方文档:why-use-process-nexttick。

Node与浏览器的 Event Loop 差异

一句话总结其中:浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。

上图来自浪里行舟

最后

来~期末考试了

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

评论区留下你的答案吧~~老铁!

参考文献

  • Tasks, microtasks, queues and schedules
  • libuv 文档
  • The Node.js Event Loop, Timers, and process.nextTick()
  • node 官网
  • async/await 在chrome 环境和 node 环境的 执行结果不一致,求解?
  • 更快的异步函数和 Promise
  • 一次弄懂Event Loop(彻底解决此类面试问题)
  • 这一次,彻底弄懂 JavaScript 执行机制
  • 不要混淆nodejs和浏览器中的event loop

相关教程