事件循环(浏览器 & Node.js)
事件循环(浏览器 & Node.js)

事件循环(浏览器 & Node.js)

事件循环(Event Loop)是让单线程Javascript实现无阻塞I/O操作的关键。

一. 前言

事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。宏任务队列可以有多个,微任务队列只有一个。

  • 常见的 macro-task 比如:script(整体代码)、setTimeout()setInterval()setImmediate()、 I/O 操作、UI 渲染等。
  • 常见的 micro-task 比如: process.nextTick()new Promise().then()new MutationObserver()等。

new Promise(callback),Promise构造器是代码同步执行的,所以callback属于‘script’。

区别

浏览器和 Node 环境下,microtask 任务队列的执行时机不同

  • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行
  • Node 端,microtask 在事件循环的各个阶段之间执行

二. 浏览器中的 Event Loop

一个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。


三. Node.js中的 Event Loop

  1. timers: 执行由setTimeout()setInterval()预定的回调。
  2. pending callbacks: 执行推迟到下一个循环迭代的I/O回调。
  3. idle, prepare: 仅供内部使用。但是可以利用空闲阶段做一些事情,比如用requestIdleCallback做react FiberTree的循环构建。
  4. poll: 检索新的I/O事件;执行与I/O相关的回调(除了close callbacks、timers、setImmediate()),node将在此处适当阻塞。当执行poll queue到空时,event loop将会检查timers中的计时器,如果存在到达时间的timers,event loop会回到timers阶段执行那些timers回调。
  5. check: 执行setImmediate()回调。
  6. close callbacks: 关闭的回调,例如socket.on('close', ...)

在每次运行事件循环之间,Node.js会检查是否有等待的异步I/O及定时器,如果没有则完全关闭。


四. 理解process.nextTick()

process.nextTick不是Event Loop的一部分。不管当前的event loop处于哪个阶段,nextTickQueue将在当前operation完成后执行,operation被定义为从底层c/c++ 处理者的过渡,处理需要被执行的JavaScript。

那么process.nextTick有什么用?

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 hasn't completed, bar hasn't been assigned any value
  console.log('bar', bar); // undefined
});

bar = 1;

通过把callback放在process.nextTick()中,script仍然具有运行完成的能力,允许在调用回调之前初始化所有变量,函数等。它还具有不允许event loop继续的优点,可能对于在事件循环被允许前向用户发出错误提示很有帮助。

let bar;

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

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

bar = 1;

另一个例子:当端口号被传递时,端口立即被绑定。因此listening回调应该被立即执行,问题是在那个时候on('listening')回调还没被设置。

const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});

为了解决这个问题,listening事件是在nextTick()中排队的,以允许脚本运行完成。这允许用户设置他们想要的任何处理程序。


五. process.nextTick() vs setImmediate()

  • process.nextTick() 在相同的‘阶段’立即触发
  • setImmediate() 在事件循环的下一个迭代或’tick’触发

本质上,他俩的名称应该互换。 process.nextTick()触发的更立即,但这是历史的产物,做交换会让很多已有的库出现问题。建议开发者在所有的情况下都使用setImmediate(),因为这样更容易推理。


六. 为什么用process.nextTick()

允许用户处理错误、清除任何不需要的资源,或者在事件循环继续之前再次尝试请求。

  1. 允许用户处理错误、清除任何不需要的资源,或者在事件循环继续之前再次尝试请求。
  2. 有时,必须允许在调用堆栈解除后但在事件循环继续之前运行回调。

运行一个从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!');
});

您不能立即从构造函数发出事件,因为script还没有处理到用户为该事件分配回调的程度。因此,在构造函数本身内,你可以使用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!');
});

发表评论

邮箱地址不会被公开。