关于EventLoop的学习

889 阅读7分钟

故事: 以下都是拾人牙慧,主要是方便自己的学习总结,因为觉得EventLoop一直是 自己的弱点,平时感觉很懵逼,就到处查资料学习一下

为什么要有Event Loop?

因为Javascript设计之初就是一门单线程语言,因此为了实现主线程的不阻塞,Event Loop这样的方案应运而生。

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

为什么要学习Event Loop

考官经常问到为什么 setTimeout 会比 Promise 后执行,明明代码写在 Promise 之前。还会出些题目,判断执行的先后顺序,这其实涉及到了 Event Loop 相关的知识

Event Loop 任务

在JavaScript中,任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask)。

MacroTask(宏任务),包括setTimeout、setInterval、setImmediate(浏览器暂时不支持,只有IE10支持)、I/O、UI交互事件 

MicroTask(微任务),包括Promise、process.nextTick(Node独有)、MutaionObserver

浏览器中的Event Loop

当我们执行 JS 代码的时候其实就是往执行栈中放入函数,那么遇到异步代码的时候该怎么办?其实当遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task

同一次事件循环中,微任务永远在宏任务之前执行。

console.log(1);

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

let promise = new Promise(resolve => {
    console.log(3);
    resolve();
}).then(data => {
    console.log(100);
}).then(data => {
    console.log(200);
});

console.log(2);

执行顺序如下

1
3
2
100
200
setTimeout

解释:

如上,按照js由上到下的执行顺序,遇到同步任务先输出1。setTimeout是宏任务,会先放到宏任务队列中。而new Promise是立即执行的,所以会先输出3。而Promise.then是微任务,会依次排列到微任务队列中,继续向下执行输出2。现在执行栈中的任务已经清空,再将微任务队列清空,依次输出100和200。之后每次取出一个宏任务,因为现在只有一个宏任务,所以最后输出setTimeout。

再看下稍微升级版的

console.log(1)

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(4)
        resolve()
    }).then(() => {
        console.log(5)
    })
})

new Promise(resolve => {
    console.log(7)
    resolve()
}).then(() => {
    console.log(8)
})

setTimeout(() => {
    console.log(9)
    new Promise(resolve => {
        console.log(11)
        resolve()
    }).then(() => {
        console.log(12)
    })
})

输出结果如下

1
7
8
2
4
5
9
11
12

分析如下:  

同步运行的代码首先输出:1、7

接着,清空microtask队列:8 

第一个task执行:2、4 

接着,清空microtask队列:5 

第二个task执行:9、11 

接着,清空microtask队列:12 

再升级一下 加上process.nextTick后在node上测试

console.log(1)

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(4)
        resolve()
    }).then(() => {
        console.log(5)
    })
    process.nextTick(() => {
        console.log(3)
    })
})

new Promise(resolve => {
    console.log(7)
    resolve()
}).then(() => {
    console.log(8)
})

process.nextTick(() => {
    console.log(6)
})

输出结果如下

1
7
6
8
2
4
3
5

这说明**process.nextTick注册的函数优先级高于Promise**

再升级一下

console.log(1)

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(4)
        resolve()
    }).then(() => {
        console.log(5)
    })
    process.nextTick(() => {
        console.log(3)
    })
})

new Promise(resolve => {
    console.log(7)
    resolve()
}).then(() => {
    console.log(8)
})

process.nextTick(() => {
    console.log(6)
})

setTimeout(() => {
    console.log(9)
    process.nextTick(() => {
        console.log(10)
    })
    new Promise(resolve => {
        console.log(11)
        resolve()
    }).then(() => {
        console.log(12)
    })
})

输出结果如下

1
7
6
8
2
4
9
11
3
10
5
12

Timer是整个Event Loop中非常重要的一环,我们先从timer切入,来切身体会下规范和实现的差异。

setTimeout(() => {
    console.log(2)
}, 2)

setTimeout(() => {
    console.log(1)
}, 1)

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

在chrome运行结果是 1 0 2

在node下运行不是很稳定 有时候是 2 1 0 有时候是 1 0 2

Node的Event Loop

Node中的Event Loop是基于libuv实现的,而libuv是 Node 的新跨平台抽象层,libuv使用异步,事件驱动的编程方式,核心是提供i/o的事件循环和异步回调。libuv的API包含有时间,非阻塞的网络,异步文件操作,子进程等等。 Event Loop就是在libuv中实现的。

不同于浏览器中的事件循环队列的实现,Node中的事件循环分的更加细致,从而有的时候会有些并不明确。Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。 

以下是网上盗图

当Node.js启动时会初始化event loop, 会做这几件事  

  • 初始化 event loop 
  • 开始执行脚本(或者进入 REPL,本文不涉及 REPL)。这些脚本有可能会调用一些异步 API、设定计时器或者调用 process.nextTick() 
  • 开始处理 event loop

每一个event loop都会包含按如下顺序六个循环阶段

  • timers 阶段会执行 setTimeout 和 setInterval callback回调,并且是由 poll 阶段控制的。同样,在 Node 中定时器指定的时间也不是准确时间,根据node的环境而变化
  • I/O callbacks回调:I/O 阶段会处理一些上一轮循环中的少数未执行的 I/O 回调,比如TCP错误,但不包括close事件、定时器和setImmediate的回调; 
  • idle, prepare:只在node内部使用 忽略不计; 
  • poll:等待新的I/O事件,node在一些特殊情况下会阻塞在这里;  
  • check:setImmediate回调在这个阶段执行; 
  • close callbacks:close callbacks 阶段执行 close 事件,只在node内部使用。


timeout、immediate 两个谁先执行不一定 取决于node的执行时间。因此定时器的执行顺序其实是随机的

---------------------------macrotask(宏任务)---------------------------------

setTimeout(function () {
    console.log('setTimeout1');
})
setImmediate(function () {
    console.log('setImmediate2');
});

执行node 时而输出setTimeout1 setImmediate2,时而输出 setImmediate2  setTimeout1

解释:

  • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的
  • 进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调
  • 那么如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了
当然在某些情况下,他们的执行顺序一定是固定的,如果i/o文件操作以后就会先执行setImmediate,因为setImmediate在i/o文件操作后面的那个阶段执行,执行完setImmediate会在下一个阶段的时候再执行setTimeout (timers 计时器执行阶段)

---------------------------macrotask(宏任务)---------------------------------

let fs = require('fs');
fs.readFile(__filename, function () {
    console.log('fs');
    setTimeout(function () {
        console.log('timeout');
    });
    setImmediate(function () {
        console.log('mmiediate');
    });
});

输出结果 fs mmiediate timeout

解释:

在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。

---------------------------microtask 微任务---------------------------------

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

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

浏览器中的输出是一样的 输出 promise1 timer21

解释:

对于 microtask 来说,它会在以上每个阶段完成前清空 microtask 队列,,microtask 永远执行在 macrotask 前面。

------------------------microtask 微任务中的process.nextTick----------------------

Promise.resolve().then(function () {
  console.log('then2'):
});
process.nextTick(function () {
  console.log('nextTick1');
});

输出结果 nextTick1 then2 

解释:

Node 中的 process.nextTick,这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。因此微任务中nextTIck 会比then先执行。

微任务会优先与i\o文件操作执行。

let fs = require('fs');
fs.readFile(__filename, function () {
    console.log('fs');
});
process.nextTick(function(){
    console.log('text2');
})

输出结果 text2  fs