EventLoop

Node的事件循环

Posted by Ray on 2018-03-27

JS执行上下文

简单的说,JS解释器在执行代码的时候会有一个执行环境,那么这个执行环境就是执行上下文。并且在从上往下的执行过程形成了一个执行上下文栈。这个栈的最底层就是 Global 全局栈。

在解释器的内部调用执行上下文(执行一个函数)的时候,分为两个阶段:

  • 创建阶段
    • 创建作用域链
    • 创建变量,函数和参数
    • 确定this的值
  • 执行阶段
    • 设置变量值和函数的引用,解释并执行代码

执行上下文的对象抽象:

1
2
3
4
5
executionContextObj = {
scopeChain: { /* 变量对象(variableObject)+ 所有父执行上下文的变量对象*/ },
variableObject: { /*函数 arguments/参数,内部变量和函数声明 */ },
this: {}
}

创建阶段的具体步骤:

  • 初始化作用域链(scopeChain)
  • variableObject中创建一个 arguments 对象(伪数组)并且设定其对应值
  • 扫描并进行函数声明,在 variableObject创建一个与函数同名的属性,其值为指向函数在内存中的指针。覆盖同名的函数申明
  • 扫描并且进行变量声明,在 variableObject创建一个与变量同名的属性,其值为Undefined。跳过已经声明的变量
  • 得出 this 的值。

这个阶段需要注意的关键问题:

  • 函数在变量之前被声明,意味着如果一个函数中有变量名和函数名同名的问题,那么即便是变量后出现,最后执行的时候得到的该变量是个函数,因为同名变量在创建阶段会跳过。

执行阶段

执行阶段将会解释并执行代码,并且对声明的变量进行等号赋值。

事件队列

执行上下文的普通代码的时候,JS都是从上到下的一次在上下文栈中执行每一句,但是当执行栈中的代码时,引擎遇到了异步操作,那么JS并不会去选择等待着异步操作结束,而是将该异步事件放进一个事件循环队列中,等到执行栈中的所有代码都执行完成之后,才会去事件队列中去寻找,队列中依次可以执行的任务,将回调函数放入执行栈中去执行。

Node 中的 Event Loop

上面的图片摘自Node官网

不同于浏览器中的事件循环队列的实现,Node中的事件循环分的更加细致,从而有的时候会有些并不明确。

首先需要明确的是这个 event-loop每个小节点(阶段)都有一些什么事件:

  • 首先可见的是 poll 作为 Event-loop 的入口,是对接下来要处理的事件进行新的轮询。
  • 接下来进入 check 阶段,处理的是所有由 setImmediate() 注册的回调。
  • close 阶段,一些监听了关闭事件的回调函数在此时执行
  • Timers,通过 setTimeout() 和 setInterval() 注册的回调会在此处处理。
  • I/O callbacks ,这才到了我们用户代码中的大部分的回调函数的执行。

此外 Event-loop 其实也是在JS的主线程上执行的,并不是像用户感受的那样是在主线程外单开了一个线程去处理事件循环。

Process.nextTick()
特殊的API,并不在 Event loop 中,他会在每个阶段结束时被调用,如果你递归的调用他,就会阻止 Event-loop 进入poll阶段,导致“饿死”之后的IO请求。

那么为什么还需要这个 Process.nextTick()?

1
2
3
4
5
6
7
8
9
10
11
12
13
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!');
});

上述代码,MyEmitter 的构造函数中想要在其内部调用一个事件,可是代码还没为该事件注册好 handle 就随着实例化MyEmitter 的同时,触发了该事件。将 MyEmitter 做如下修改:

1
2
3
4
5
6
7
8
function MyEmitter() {
EventEmitter.call(this);

// use nextTick to emit the event once a handler is assigned
process.nextTick(() => {
this.emit('event');
});
}

该代码就会在模块代码操作完成之后才会触发 ‘event’ 事件,此时你已经注册好了,回调处理函数。

Timers 和 Check

1
2
3
4
5
6
7
setTimeout(() => {
console.log('timeout');
}, 0);

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

对于上面的代码,setTimeout 和 setImmediate 如果运行在主模块中的时候,他们的执行顺序是不确定的,取决于不受控制的进程性能限制。而当他们出现在 IO 声明周期中时 setImmediate 总是先执行(对应上面的阶段图):

1
2
3
4
5
6
7
8
9
10
const fs = require('fs');

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

参考:

What is the Execution Context & Stack in JavaScript?
[译] 所有你需要知道的关于完全理解 Node.js 事件循环及其度量

本文为原创文章作为学习交流笔记,如有错误请您评论指教
转载请注明来源:https://isliulei.com/article/eventLoop/