Zijue / blog

personal knowledge collection

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

3.node 事件环

Zijue opened this issue · comments

commented

什么是 node 事件环

事件环是 node 处理非阻塞 I/O 操作的机制。尽管 js 是单线程处理的,但是当可能的时候,它们会把操作转移到系统内核中去,目前大多数内核都是多线程的,它们可以在后台处理多种操作,当其中的一个操作完成的时候,内核通知 node 将合适的回调函数添加到轮询队列中等待时机执行。

node 事件环执行流程解析

node官网对 node 事件环的说明:https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/

在解析 node 事件环之前,了解以下两点

  • 浏览器和 node 事件环在 node v11 版本及更高版本中,执行结果都一样,本质上不一样
  • node 中有多个宏任务队列

下面通过图例的方式以便于更好的理解事件环的执行流程

注意:每个框被称为事件环机制的一个阶段

如上图所示,event loop 总共有六个阶段,每个阶段都有一个 FIFO 队列来执行回调。当事件环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列清空或执行了最大回调数,当该队列清空或达到回调限制,事件环将进入到下一阶段。

阶段概述

  • timers 执行定时器 setTimeout()setInterval() 的回调函数
  • pending callbacks 执行延迟到下一个循环迭代的 I/O 回调
  • idle, prepare 仅系统内部使用
  • poll 检索新的 I/O 事件,执行与 I/O 相关的回调(主要存放的异步 I/O 操作)。node 中基本上所有的异步 api 的回调都会在这个阶段来处理。node 将在适当的时候在此阻塞
  • check 执行 setImmediate() 的回调函数
  • close callbacks 执行一些关闭的回调函数,如:socket.on('close', ...)

默认是从上到下依次执行代码,依次清空每个队列中的回调方法,每调用一个宏任务都会清空微任务队列。

主栈 => 检测定时器中有没有到达的定时回调,有就执行(每执行一个宏任务都会清空微任务队列)=> 进入 poll 阶段(I/O 操作),逐一清空 => 检测 check 阶段队列中是否有 setImmediate 的回调,如果则进入 check 阶段并清空队列,如果没有就会在 poll 阶段阻塞 => 不停的看定时器队列中是否有回调函数,如果有则进入 timers 阶段执行

看到一位大佬写的关于 node event loop 的 blog,借用其中一张描述 event loop 的图

setImmediate() 对比 setTimeout()

  • setImmediate() 设计为一旦在当前 poll 阶段完成, 就执行脚本
  • setTimeout() 在指定的时间阀值(单位 ms)过后运行脚本

执行计时器的顺序将根据调用它们的上下文而异

如果运行以下不在 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 循环内调用,setImmediate 总是被优先调用

// 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

使用 setImmediate() 相对于 setTimeout() 的主要优势是,如果 setImmediate() 是在 I/O 周期内被调度的,那它将会在其中任何的定时器之前执行,跟这里存在多少个定时器无关

process.nextTick

process.nextTick 是 node 实现的异步 API 的一部分,从技术上讲它不是事件环的一部分。不管事件环在哪个阶段,它在当前操作完成后处理并清空 nextTickQueue,优先于 microTaskQueue (微任务队列)

let bar;

// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall(callback) { callback(); }

// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {
  // since someAsyncApiCall has completed, bar hasn't been assigned any value
  console.log('bar', bar); // undefined
});

bar = 1;
let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar', bar); // 1
});

bar = 1;

对比这两段代码,通过将回调置于 process.nextTick() 中,脚本仍具有运行完成的能力,同时允许在调用回调之前初始化所有的变量、函数等。它还具有不让事件循环继续的优点,适用于让事件循环继续之前,警告用户发生错误的情况