做题学知识(3)之 Event Loop
问题
第一题
问多久会弹出来 ok?
let val = true
setTimeout(function () {
val = false
}, 3000)
while(val){}
alert('OK')
第二题
问多久会打印出来 1
setTimeout(function () {
console.log(1)
}, 3000)
for(....) // 一个执行 5000ms 的for 循环
第三题
问打印顺序是什么样的?
setTimeout(function () {
console.log(1)
}, 3000)
setTimeout(function () {
console.log(2)
}, 1000)
for(....) // 一个执行 5000ms 的 for 循环
答案
因为 JS 是单线程的要想实现异步操作需要采用一种机制这种机制就是 Event Loop,而上面的三道题主要考察了这个知识点。先来看一张图
可以看到图中主要分为了四部分:Heap(堆),Stack(栈),Queue(队列),WebAPIs。其中的 Heap(堆)可以不用考虑,因为不影响本文的阅读。(这里不讨论宏任务和微任务,微任务会在扩展阅读中加入)
当 JS 代码开始执行的时候就是开始一个循环:Stack(栈)中为空,Queue(队列)中有一个 main 主函数,将 main 函数放入 Stack(栈)中,在主函数中会碰到一些 WebAPIs,如果是异步的通常都会有个 callback function(回调函数)当异步完成之后 WebAPIs 会将这个callback function(回调函数)放入 Queue(队列)中,当 Stack(栈)中的内容全部出栈的时候就开始下一个循环将 Queue(队列)第一个任务放入栈中以此循环执行下去。
第一题
问多久会弹出来 ok?
let val = true
setTimeout(function () {
val = false
}, 3000)
while(val){}
alert('OK')
分析一下代码的执行情况:
-
开始状态:Stack[],Queue[Main]
-
第一次循环将 Queue 的第一个任务放到 Stack 中: Stack[Main],Queue[]
-
由于 Main 中有 setTimeout 所以放到 WebAPIs 中以待合适的时机将 callback function 放入 Queue 中
Main 中的代码简化之后如下:
let val = true
while(val){}
alert('OK')
可以看到里面有个 while 的死循环,因此 Stack 中的代码永远执行不完。当 3000ms 之后。WebAPIs 处理完成会将 setTimeout 入队 Queue[setTimeout],但是由于 Stack 中的代码永远执行不完因此下一个 Event 循环不会开启。答案是永远不会弹出
第二题
问多久会打印出来 1
setTimeout(function () {
console.log(1)
}, 3000)
for(....) // 一个执行 5000ms 的for 循环
分析一下代码的执行情况:
-
开始状态:Stack[],Queue[Main] -
第一次循环将 Queue 的第一个任务放到 Stack 中: Stack[Main],Queue[] -
由于 Main 中有 setTimeout 所以放到 WebAPIs 中以待合适的时机将 callback function 放入 Queue 中 -
代码执行 3000ms 的时候 setTimeout 执行完成入队:Stack[Main], Queue[setTimeout] -
代码执行 5000ms 的时候栈中代码全部完成: Stack[],Queue[SetTimeout] -
开始下一个循环, Queue 队列中第一个任务出队入栈:Stack[setTimeout],Queue[] -
栈中代码执行,1 会打印出来,**因此答案是 5000ms **
第三题
问打印顺序是什么样的?
// 我在 Queue 中表示为 set1
setTimeout(function () {
console.log(1)
}, 3000)
// 我在 Queue 中表示为 set2
setTimeout(function () {
console.log(2)
}, 1000)
for(....) // 一个执行 5000ms 的 for 循环
分析一下代码的执行情况:
-
开始状态:Stack[],Queue[Main] -
第一次循环将 Queue 的第一个任务放到 Stack 中: Stack[Main],Queue[] -
由于 Main 中有 setTimeout 所以放到 WebAPIs 中以待合适的时机将 callback function 放入 Queue 中 -
代码执行 1000ms 的时候 set2 的 WebAPIs 执行完成:Stack[Main], Queue[set2] -
代码执行 3000ms 的时候 set1 的 WebAPIs 执行完成: Stack[Main],Queue[set2, set1] -
代码执行到 5000ms 的时候 Stack 中代码执行完成,Queue 中第一个任务出队进入 Stack 中:Stack[set2], Queue[set1] -
如此执行因此 答案是:2,1
扩展阅读
这里借用一下 《JavaScript 忍者秘籍》的图,如果所示这就是一个完整的 Event Loop 牵扯到的东西,可以简单理解为将之前 Queue 分为了三个部分 Task(宏任务)队列、microTask(微任务)队列、UI渲染。循环步骤如下:
-
将 Task 队列中的第一个 Task 压入栈执行,第一个宏任务是 Main 函数 -
Main 函数执行完成,依次执行 microTask 中所有的任务 -
microTask 中所有的任务执行完成进行一次 UI 渲染 -
再接着将 Task 队列中的第一个 Task 压入栈,如此反复执行
你可能要问了,为什么要有 Task 和 microTask 之分?这里你要考虑俩个常见的场景:
-
Task 中的任务是一直往后排的,当你某个任务想要快速响应的时候你是做不到的,因此你用一个 microTask 就能够插队优先执行 -
UI 渲染是最后执行的,你有时候想要在本次渲染完成之前进行一次数据变更。这时候也需要用到 microTask
为了加深理解这里也来几道题:
setTimeout(() => {
console.log(1)
}, 0)
<!--这个在 microTask 中叫 P1-->
Promise.resolve().then(() => {
console.log(2)
<!--这个在 microTask 中叫 P2-->
Promise.resolve().then(() => {
console.log(3)
})
})
setTimeout 和 Promise 都是异步队列,请问输出情况是什么样子的?
答案是:2,3,1
答案解析:
-
开始状态: Stack[],Task[Main],microTask[] -
Task 队列第一个 Task 压入栈: Stack[Main],Task[],microTask[] -
栈执行完毕: Stack[], Task[setTimeout], microTask[P1] -
开始依次执行 microTask 队列中的所有任务:P1 执行,打印 2,发现新的 microTask 加入 microTask 队列: Stack[], Task[setTimeout], microTask[P2] -
第一个 microTask 执行完毕,检查 microTask 队列是否还有,发现还有 P2,执行,打印 3:Stack[], Task[setTimeout], microTask[] -
microTask 队列中没有任务,检测是否需要 UI 渲染,一轮循环完成,将 Task 队列中的第一个 Task 压入栈:Stack[setTimeout], Task[], microTask[] -
栈中代码执行完成,打印 1:Stack[], Task[], microTask[] -
等待新的 Task 进入 Task 队列
总结
整个 Event Loop 过程是有迹可循的,关键是明白这个过程。并且区分出来哪些异步任务是 Task 和 microTask