JavaScript中的Event Loop

691 阅读5分钟

一、Event Loop是什么?

概念:

Event Loop即事件循环,是一个执行模型,主线程从"任务队列"中读取执行事件,这个过程是循环不断的,这个机制被称为事件循环,

目的:

是浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。

二、为什么会有Event Loop?

javascript从诞生之日起就是一门单线程的非阻塞的脚本语言,单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务,也就是说,同一个时间只能做一件事,所以js中有事件循环,是因为js是单线程的原因。

三、Event Loop模型有哪些?

在不同的地方有不同的实现,浏览器和NodeJS基于不同的技术实现了各自的Event Loop。

**浏览器:**Event Loop是在html5的规范中明确定义。

Node-JS:Event Loop是基于libuv实现的。可以参考Node以及libuv的官方文档。

libuv已经对Event Loop做出了实现,而HTML5规范中只是定义了浏览器中Event Loop的模型,具体的实现留给了浏览器厂商。

四、任务队列(task queue)

是一种先进先出的一种数据结构。

由于js是单线程的,只有当上一个任务完成之后才会继续完成下一个任务,如果前一个任务耗时很长,后一个任务就不得不一直等着。于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

**同步任务:**在调用栈中按照顺序等待主线程依次执行(在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务);

**异步任务:**不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

宏任务(macrotask )和微任务(microtask ),表示异步任务的两种分类

**宏任务:**可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行,每一个宏任务会从头到尾将这个任务执行完毕,不会执行其它),包括以下内容:
  • setTimeout

  • setInterval

  • setImmediate (Node独有)

  • requestAnimationFrame (浏览器独有)

  • I/O

  • UI rendering (浏览器独有)

  • 包括整体代码 script

微任务:,可以理解是在当前 task 执行结束后立即执行的任务 ,包括以下内容:

  • process.nextTick (Node独有)

  • Promise

  • Object.observe

  • MutationObserver

五、浏览器的Event Loop

注意:

  • 宏任务macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
  • 当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件, 然后再去宏任务队列中取出一个事件。同一次事件循环中, 微任务永远在宏任务之前执行
  • 在执行微队列microtask queue中任务的时候,如果又产生了microtask,那么会继续添加到队列的末尾,也会在这个周期执行,直到microtask queue为空停止。

先看一个简单的示例:

setTimeout(()=>{
    console.log("setTimeout1");
    Promise.resolve().then(data => {
        console.log(222);
    });
});
setTimeout(()=>{
    console.log("setTimeout2");
});
Promise.resolve().then(data=>{
    console.log(111);
});

思考一下, 运行结果是什么?

运行结果为:

111
setTimeout1
222
setTimeout2

再思考一下下面代码的执行顺序:

console.log('script start');

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

setTimeout(function () {
    console.log('setTimeout---200');
    setTimeout(function () {
        console.log('inner-setTimeout---0');
    });
    Promise.resolve().then(function () {
        console.log('promise5');
    });
}, 200);

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

思考一下, 运行结果是什么?

运行结果为:

script start
script end
promise1
promise3
promise2
setTimeout---0
setTimeout---200
promise5
inner-setTimeout---0

五、NodeJS的Event Loop

在node中,事件循环表现出的状态与浏览器中大致相同。不同的是node中有一套自己的模型。node中事件循环的实现是依靠的libuv引擎。我们知道node选择chrome v8引擎作为js解释器,v8引擎将js代码分析后去调用对应的node api,而这些api最后则由libuv引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。 因此实际上node中的事件循环存在于libuv引擎中。

NodeJS的Event Loop,执行宏队列的回调任务有6个阶段,如下图:

从上图模型中,我们可以大致分析出node中事件循环顺序:

外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段...

阶段功能:

  • timers: 这个阶段执行定时器队列中的回调如 setTimeout() 和 setInterval()。
  • I/O callbacks: 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和setImmediate()的回调。
  • idle, prepare: 这个阶段仅在内部使用,可以不必理会。
  • poll: 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
  • check: setImmediate()的回调会在这个阶段执行。
  • close callbacks: 例如socket.on('close', ...)这种close事件的回调。

NodeJS的Event Loop过程:

  • 执行全局Script的同步代码

  • 执行microtask微任务,先执行所有Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务

  • 开始执行macrotask宏任务,共6个阶段,从第1个阶段开始执行相应每一个阶段macrotask中的所有任务,注意,这里是所有每个阶段宏任务队列的所有任务,在浏览器的Event Loop中是只取宏队列的第一个任务出来执行,每一个阶段的macrotask任务执行完毕后,开始执行微任务;

六、NodeJS与浏览器的Event Loop主要差异

浏览器:microtask的任务队列是每个macrotask执行之后执行。

nodejs:microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。