Even Loop
JavaScript 是单线程的,所有 JavaScript 的任务是按次序执行的,虽然我们的 JavaScript 却有异步执行的方案。但是因为 JavaScript 是单线程的语言,所有的异步都是通过同步模拟的。
JavaScript 的任务分为两种:同步任务 和 异步任务。
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片、音乐之类的占用资源大耗时久的任务就是执行的异步任务。所以我们经常看到图片的延迟加载(图片一般使用预加载和懒加载技术)。
导图要表达的内容用文字来表述的话:
- 同步和异步任务分别进入不同的执行“场所”,同步的进入主线程, 异步的进入 Event Table 并注册函数。
- 当指定的事情完成时,Event Table 会将这个函数移入 Event Quere。
- 主线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程。
- 上述过程会不断重复,也就是常说的 Event Loop(事件循环)。
异步是使用场景
- 定时任务:setTimeout、setInterval
- 网络请求:Ajax请求,动态加载
- 事件绑定
setTimeout 的执行机制
setTimeout(() => {
task()
},3000)
sleep(10000000)
这里我们本意是 3s 后执行 task() ,但是我们却发现控制台执行 task() 需要的时间远远超过了 3s 。这是因为这里的 3s 只是 task() 进入 Event Queue 的时间,如果主线程中还有其他函数在执行(sleep),task() 是排在这个函数后面的,即使说它要等待前面的函数执行完才会进入主线程执行。过程如下:
- task() 进入 Event Table 并注册,计时开始。
- 执行 sleep 函数,task() 计时继续。
- 3s 后,timeout 计时事件完成,task() 进入 Event Queue,但是 sleep() 还在执行,task() 继续等待。
- sleep() 执行完成,task() 从 Event Queue 中进入主进程执行。
setTimeout(fn, 0) 不是 0s 后立即执行,而是等待主线程最早获得空闲时间后执行。但实际上,就算主线程是空,0ms 也是达不到了,最低是 4ms。
setInterval 的执行机制
setInterval 和 setTimeout 类似,不过 setInterval 是循环的执行。对于执行顺序来说,setInterval 会每隔指定的时间将注册的函数置入 Event Queue,如果前面的任务耗时太久,同样需要等待。
需要注意的是,对于 setInterval(fn, ms) 来说,不是每过 ms 执行一次,而是每过 ms 会将 fn 如 Event Queue。所有,一旦 setInterval 的回调函数 fn 执行的时间超过了延迟时间 ms,那么就完全看不出间隔!
Promise 和 process.nextTick(callback) 的执行机制
除了广义的同步任务和异步任务,我们对任务还有更精细的定义:
- macro-task(宏任务):包括整体代码:script,setTimeout,setInterval
- micro-task(微任务):包括 Promise,process。nextTick
不同的任务会进入其相应的 Event Queue。
事件循环的顺序,决定 js 代码执行的顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。
如下代码:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
第一轮事件循环流程分析如下:
- 整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
- 遇到
setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1。 - 遇到
process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1。 - 遇到Promise,new Promise直接执行,输出7。then 被分发到微任务Event Queue中。我们记为
then1。 - 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为
setTimeout2。
| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| setTimeout1 | process1 |
| setTimeout2 | then1 |
- 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
- 我们发现了
process1和then1两个微任务。 - 执行
process1,输出6。 - 执行
then1,输出8。
第二轮事件循环流程分析如下:
好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:
- 首先输出2。接下来遇到了
process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。
| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| setTimeout2 | process2 |
| then2 |
- 第二轮事件循环宏任务结束,我们发现有
process2和then2两个微任务可以执行。 - 输出3。
- 输出5。
- 第二轮事件循环结束,第二轮输出2,4,3,5。
第三轮事件循环流程分析如下:
第三轮事件循环开始,此时只剩setTimeout2了,执行。
- 直接输出9。
- 将
process.nextTick()分发到微任务Event Queue中。记为process3。 - 直接执行
new Promise,输出11。 - 将then分发到微任务Event Queue中,记为then3。
| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| process3 | |
| then3 |
- 第三轮事件循环宏任务执行结束,执行两个微任务
process3和then3。 - 输出10。
- 输出12。
- 第三轮事件循环结束,第三轮输出9,11,10,12。
整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)