来自 Web前端 2020-04-29 17:43 的文章
当前位置: 网上澳门金莎娱乐 > Web前端 > 正文

浅析Nodejs 网上澳门金莎娱乐:Event Loop

时间: 2019-09-07阅读: 155标签: 任务写在前面

什么是事件循环(Event Loop)

事件循环能让 Node.js 执行非阻塞 I/O 操作,尽管JavaScript事实上是单线程的,通过在可能的情况下把操作交给操作系统内核来实现。

由于大多数现代系统内核是多线程的,内核可以处理后台执行的多个操作。当其中一个操作完成的时候,内核告诉 Node.js,相应的回调就被添加到轮询队列(poll queue)并最终得到执行。

写这篇文章的原因是因为,这几天在看 core-js 的源码,然后发现了 queueMicrotask 的实现。由于之前做的项目,对于微任务的执行需求,一般是使用 asap 这个库来完成的,如果没有使用这个库的话,简易版本可以通过 Promise.resolve() 来代替,并没有接触过过这个 api,所以就抽时间研究一下。

阶段总览

在node中事件每一轮循环按照顺序分为6个阶段,来自libuv的实现:

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:                      │
│  │         poll          │<─────────────── ┤  connections,                 │
│  └──────────┬────────────┘      │   data, etc.                      │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

timers 阶段: 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;
I/O callbacks 阶段: 是否有已完成的I/O操作的回调函数,来自上一轮的poll残留;
idle, prepare 阶段: 仅node内部使用;
poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;
check 阶段: 执行setImmediate() 设定的callbacks;
close callbacks 阶段: 比如socket.on(‘close’, callback)的callback会在这个阶段执行.
每一个阶段都有一个装有callbacks的fifo queue(队列),当event loop运行到一个指定阶段时,
node将执行该阶段的fifo queue(队列),当队列callback执行完或者执行callbacks数量超过该阶段的上限时,event loop会转入下一下阶段.

上面六个阶段都不包括 process.nextTick(),process.nextTick不是基于libuv事件机制的,而timers一系列的api全部是基于libuv开放出来的api实现的。那么这个nextTick到底是如何实现的呢?我们先打个问号.

兼容性

阶段细节

以下各阶段细节部分介绍来自原文:
The Node.js Event Loop

一般看这种偏 web 标准的新的api,肯定上来要先看兼容性的,我去caniuse查了一下,wtf? 竟然搜索无结果。(详见issue)

定时器(timers)

定时器的用途是让指定的回调函数在某个阈值后会被执行,具体的执行时间并不一定是那个精确的阈值。定时器的回调会在制定的时间过后尽快得到执行,然而,操作系统的计划或者其他回调的执行可能会延迟该回调的执行。

然后只能去 MDN 来看一下了,大概是下图这个样子:

轮询(poll)

轮询阶段有两个主要功能:
1,执行已经到时的定时器脚本
2,处理轮询队列中的事件

当事件循环进入到轮询阶段却没有发现定时器时:
如果轮询队列非空,事件循环会迭代回调队列并同步执行回调,直到队列空了或者达到了上限(前文说过的根据操作系统的不同而设定的上限)。
如果轮询队列是空的:
如果有setImmediate()定义了回调,那么事件循环会终止轮询阶段并进入检查阶段去执行定时器回调;
如果没有setImmediate(),事件回调会等待回调被加入队列并立即执行。
一旦轮询队列空了,事件循环会查找已经到时的定时器。如果找到了,事件循环就回到定时器阶段去执行回调。

可以发现还是比较新的api,如果要在项目中直接使用的话,还是建议导入polyfill或者使用asap这个库来实现类似的需求。

I/O callbacks

这个阶段执行一些诸如TCP错误之类的系统操作的回调。例如,如果一个TCP socket 在尝试连接时收到了 ECONNREFUSED错误,某些 *nix 系统会等着报告这个错误。这个就会被排到本阶段的队列中。

为什么我们需要这个api?

检查(check)

这个阶段允许回调函数在轮询阶段完成后立即执行。如果轮询阶段空闲了,并且有回调已经被 setImmediate() 加入队列,事件循环会进入检查阶段而不是在轮询阶段等待。
setImmediate() 是个特殊的定时器,在事件循环中一个单独的阶段运行。它使用libuv的API 来使得回调函数在轮询阶段完成后执行。

从微任务本身的概念来说的话,就是当我们期望某段代码,不阻塞当前执行的同步代码,同时又期望它尽可能快地执行时,我们就需要它(这里不再赘述微任务的概念,可以参考这篇文章)。

关闭事件的回调(close callbacks)

如果一个 socket 或句柄(handle)被突然关闭(is closed abruptly),例如 socket.destroy(), 'close' 事件会被发出到这个阶段。否则这种事件会通过 process.nextTick() 被发出。

一般情况下,如果是编写业务代码,我觉的很少会遇到这样的需求,唯一能想到的情况可能存在于一些对即时反馈有性能要求的场景,比如搜索,当输入关键字后发送异步请求获取搜索信息之后,我们可能会在前端对搜索结果进行一些处理,比如排序或者分组,但是这些操作可能不是优先级最高的任务,但它们又比较耗时(比如排序),因此我们可能期望推迟它们的执行,但又期望它们尽可能早地执行。

setTimeout VS setImmediate

二者非常相似,但是二者区别取决于他们什么时候被调用.

setImmediate 设计在poll阶段完成时执行,即check阶段;
setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行;但其在timer阶段执行
其二者的调用顺序取决于当前event loop的上下文,如果他们在异步i/o callback之外调用,其执行先后顺序是不确定的

setTimeout(function timeout () {
  console.log('timeout');
},0);

setImmediate(function immediate () {
  console.log('immediate');
});

$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

为什么结果不确定呢?

解释:setTimeout/setInterval 的第二个参数取值范围是:[1, 2^31 - 1],如果超过这个范围则会初始化为 1,即 setTimeout(fn, 0) === setTimeout(fn, 1)。我们知道 setTimeout 的回调函数在 timer 阶段执行,setImmediate 的回调函数在 check 阶段执行,event loop 的开始会先检查 timer 阶段,但是在开始之前到 timer 阶段会消耗一定时间,所以就会出现两种情况:

timer 前的准备时间超过 1ms,满足 loop->time >= 1,则执行 timer 阶段(setTimeout)的回调函数
timer 前的准备时间小于 1ms,则先执行 check 阶段(setImmediate)的回调函数,下一次 event loop 执行 timer 阶段(setTimeout)的回调函数
再看个例子:

setTimeout(() => {
  console.log('setTimeout')
}, 0)

setImmediate(() => {
  console.log('setImmediate')
})

const start = Date.now()
while (Date.now() - start < 10);

运行结果一定是:

setTimeout
setImmediate

在阅读一些著名框架或者工具库的过程中,我发现很多情况下作者都会遇到这个需求,一般都通过process.nextTick或者Promise.resolve来解决。

process.nextTick实现

process.nextTick不是基于libuv事件机制的,而timers一系列的api全部是基于libuv开放出来的api实现的。那么这个nextTick到底是如何实现的呢?
接下来就要从nextTick的源码聊起了:

function _tickCallback() {
    let tock;
    do {
      while (tock = nextTickQueue.shift()) {
      // ...
      const callback = tock.callback;
        if (tock.args === undefined)
          callback();
     // ...
     }
      runMicrotasks();
    } 
  // ...
  }

在执行完nextTick之后(callback())还继续执行了runMicrotasks,我相信如果了解过Microtasks的读者肯定知道这到底是做什么的,接下来我们深扒一下这个runMicrotasks:

// src/node.cc
v8::Local<v8::Function> run_microtasks_fn =
    env->NewFunctionTemplate(RunMicrotasks)->GetFunction(env->context())
        .ToLocalChecked();//v8 吐出来的方法 RunMicrotasks
run_microtasks_fn->SetName(
    FIXED_ONE_BYTE_STRING(env->isolate(), "runMicrotasks"));

// deps/v8/src/isolate.cc
void Isolate::RunMicrotasks() {// v8中RunMicrotasks实现
// Increase call depth to prevent recursive callbacks.
v8::Isolate::SuppressMicrotaskExecutionScope suppress(
    reinterpret_cast<v8::Isolate*>(this));
is_running_microtasks_ = true;
RunMicrotasksInternal();
is_running_microtasks_ = false;
FireMicrotasksCompletedCallback();
}
void Isolate::RunMicrotasksInternal() {
if (!pending_microtask_count()) return;
TRACE_EVENT0("v8.execute", "RunMicrotasks");
TRACE_EVENT_CALL_STATS_SCOPED(this, "v8", "V8.RunMicrotasks");
while (pending_microtask_count() > 0) {
  HandleScope scope(this);
  int num_tasks = pending_microtask_count();
  Handle<FixedArray> queue(heap()->microtask_queue(), this);
  DCHECK(num_tasks <= queue->length());
  set_pending_microtask_count(0);
  heap()->set_microtask_queue(heap()->empty_fixed_array());
// ...

通过上面的代码,可以比较清晰地看到整个RunMicrotasks的全过程,主要就是通过microtask_queue来实现的Microtask。
了解了整个流程,可以很容易得出一个结论:nextTick会在v8执行Microtasks之前对在js中注册的nextTickQueue逐个执行,即阻塞了Microtasks执行。
后面简单的追踪了一下_tickCallback来证实一下最终_tickCallback传递给了tick_callback_function,追踪的过程小伙伴们有兴趣可以自行了解,最终可以总结为以下两点:
1.nextTick永远在主函数(包括同步代码和console)运行完之后运行
2.nextTick永远优先于microtask运行

它和setTimeout的区别?

浏览器Event Loop

浏览器中与node中事件循环与执行机制不同,不可混为一谈。 浏览器的Event loop是在HTML5中定义的规范,而node中则由libuv库实现。

macrotasks: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promises, Object.observe(废弃), MutationObserver

本质上的区别应该在它们的执行时机上,而执行时机上的区别,本质上就是微任务和宏任务的区别。可以直接打开控制台运行一下以下的代码:

浏览器执行过程

执行完主执行线程中的任务。
取出Microtask Queue中任务执行直到清空。
取出Macrotask Queue中一个任务执行。
取出Microtask Queue中任务执行直到清空。
重复3和4。

即为同步完成,一个宏任务,所有微任务,一个宏任务,所有微任务......

setTimeout(() = { console.log('setTimeout');}, 0);queueMicrotask(() = { console.log('queueMicrotask');}); 

来看个例子

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

运行结果是:

script start
script end
promise1
promise2
setTimeout

解析:一开始task队列中只有script,则script中所有函数放入函数执行栈执行,代码按顺序执行。
接着遇到了setTimeout,它的作用是0ms后将回调函数放入task队列中,也就是说这个函数将在下一个事件循环中执行(注意这时候setTimeout执行完毕就返回了)。
接着遇到了Promise,按照前面所述Promise属于microtask,所以第一个.then()会放入microtask队列。
当所有script代码执行完毕后,此时函数执行栈为空。开始检查microtask队列,此时队列不为空,执行.then()的回调函数输出'promise1',由于.then()返回的依然是promise,所以第二个.then()会放入microtask队列继续执行,输出'promise2'。
此时microtask队列为空了,进入下一个事件循环,检查task队列发现了setTimeout的回调函数,立即执行回调函数输出'setTimeout',代码执行完毕。

以上便是浏览器事件循环的过程
更多文献可参考:
浏览器和Node不同的事件循环

运行结果不出意外应该是:

总结

网上澳门金莎娱乐 1

event loop.jpg

绿色小块是 macrotask(宏任务),macrotask 中间的粉红箭头是 microtask(微任务)。

1.nodejs event loop分了六个不同的阶段执行,而process.nextTick()不在event loop的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行。
2.nodejs event loop 执行过程
清空当前循环内的Timers Queue,清空NextTick Queue,清空Microtask Queue。
清空当前循环内的I/O Queue,清空NextTick Queue,清空Microtask Queue。
清空当前循环内的Check Queu,清空NextTick Queue,清空Microtask Queue。
清空当前循环内的Close Queu,清空NextTick Queue,清空Microtask Queue。
进入下轮循环。
3.process.nextTick为代表的 microtask执行仍然将 tick 函数注册到当前 microtask 的尾部,而setTimeout等宏任务会注册到下一次事件循环中。
4.浏览器中与node中事件循环与执行机制不同,浏览器的eventloop是在HTML5中定义的规范,而node中则由libuv实现。

queueMicrotasksetTimeout

如果你熟悉nodejs的话,应该和process.nextTick是类似的。

使用其他方式进行模拟所带来的问题?

这也是我一开始脑海中出现的问题,就是既然我们已经可以通过别的方式来模拟微任务的执行,我们还需要这个api干什么?比如,通过下面的代码:

setTimeout(() = { console.log('setTimeout');}, 0);Promise.resolve().then(() = { console.log('queueMicrotask');}); 

会得到和上面代码一样的运行结果。

本文由网上澳门金莎娱乐发布于Web前端,转载请注明出处:浅析Nodejs 网上澳门金莎娱乐:Event Loop

关键词: