浏览器的event loop和node的event loop

2,827 阅读4分钟

1.什么是event loop

event loops也就是事件循环,它是为了协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking),用户代理(user agent)的工作而产生的一个机制。

2.JavaScript的运行机制

2.1 单线程的JavaScript

JavaScript语言的一大特点就是单线程,也就是说在同一时间只做同一件事。这是基于js的执行环境决定的,因为在浏览器中,有许多的dom操作,如果在同一时间操作一个dom,很容易造成混乱,所以为了避免发生同一时间操作同一dom的情况,js选择只用一个主线程执行代码,来保证程序执行的一致性,单线程的特点也应用到了node中。

2.2 JavaScript中的任务和队列

JavaScript是单线程的,也就意味着所有任务需要排队,前一个任务执行完,才能执行下一个任务,但是因为IO设备(输入输出设备)很慢(比如Ajax从网络读取数据),不得不等待结果返回之后才能继续,这样的执行效率很慢。 于是分成了两种任务来处理,同步任务和异步任务。 同步任务是指在主线程排队的任务,只有前面的任务执行完之后才执行后面的任务。 异步任务指的是任务不进入主线程,而进入到一个任务队列(task queue),主线程的任务可以继续往后执行,而在任务队列里的异步任务执行完会通知主线程。

3.浏览器的event loop

3.1执行栈与事件队列

当javascript代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)和栈(stack)中来加以区分。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针。当所有所有同步任务都在主线程上执行时,这些任务被排列在一个单独的地方,形成一个执行栈

当浏览器js引擎解析这段代码时,会将同步任务顺序加入执行栈中依次执行,当遇到异步任务时并不会一直等待异步任务返回结果再执行后面的任务,而是将异步任务挂起,继续执行同步任务,当异步任务返回结果时,将异步任务的回调事件加入到一个事件队列(Task Queue)当中去,这个事件队列里的任务并不会立即执行,而是等同步任务全部执行完,再依次执行事件队列里的事件。

依次执行同步任务,完成后依次执行事件队列,完成后再去执行同步任务,这样形成了一个循环,就是事件循环(Event Loop)。

3.2宏任务(macro task)与微任务(micro task)

异步任务又分为宏任务与微任务两种,微任务并不是老老实实的按照事件队列的顺序去执行,而是按照microTask—>macroTask的顺序去执行,先执行完队列中所有的microTask再去执行macroTask

宏任务和微任务的分类

  • MacroTask: script(整体代码), setTimeout, setInterval, setImmediate(node独有), I/O, UI rendering

  • MicroTask: process.nextTick(node独有), Promises, Object.observe(废弃), MutationObserver

举个例子
setTimeout(()=>{
    console.log(1)
})

Promise.resolve().then(function() {
    console.log(2)
})
console.log(3)

执行结果是:3 2 1
这是因为事件循环的顺序是:同步代码=>微任务=>宏任务

4.node的event loop

  • timers: 这个阶段执行定时器队列中的回调如 setTimeout() 和 setInterval()。

  • I/O callbacks: 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和setImmediate()的回调。

  • idle, prepare: 这个阶段仅在内部使用,可以不必理会。

  • poll: 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。

  • check: setImmediate()的回调会在这个阶段执行。

  • close callbacks: 例如socket.on('close', ...)这种close事件的回调。

event loop的每一次循环都需要依次经过上述的阶段。 每个阶段都有自己的callback队列,每当进入某个阶段,都会从所属的队列中取出callback来执行,当队列为空或者被执行callback的数量达到系统的最大数量时,进入下一阶段。这六个阶段都执行完毕称为一轮循环。

举个例子(1)
浏览器与Node执行顺序的区别
setTimeout(()=>{
    console.log('timer1')

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

setTimeout(()=>{
    console.log('timer2')

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

浏览器输出: time1 promise1 time2 promise2 因为promise是microtask,所以当第一个setTimeout执行完之后,先执行promise。

Node输出: time1 time2 promise1 promise2 因为time1和time2都在timers阶段,所以先执行timers,promise的回调被加入到了microtask队列,等到timers阶段执行完毕,在去执行microtask队列。

举个例子(2)
MicroTask队列与MacroTask队列
setTimeout(function () {
   console.log(1);
});
console.log(2);
process.nextTick(() => {
   console.log(3);
});
new Promise(function (resolve, rejected) {
   console.log(4);
   resolve()
}).then(res=>{
   console.log(5);
})
setImmediate(function () {
   console.log(6)
})
console.log('end');

node输出的顺序是 2 4 end 3 5 1 6 首先执行的是同步任务中的2 4 end,然后是microTask队列中的process.nextTick:3、promise.then:5,最后是macroTask队列中的setTimeout:1、setImmediate:6,由于Timer优于Check阶段,所以先1后6。