阅读 166

做题学知识(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')
复制代码

分析一下代码的执行情况:

  1. 开始状态:Stack[],Queue[Main]

  2. 第一次循环将 Queue 的第一个任务放到 Stack 中: Stack[Main],Queue[]

  3. 由于 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 循环
复制代码

分析一下代码的执行情况:

  1. 开始状态:Stack[],Queue[Main]
  2. 第一次循环将 Queue 的第一个任务放到 Stack 中: Stack[Main],Queue[]
  3. 由于 Main 中有 setTimeout 所以放到 WebAPIs 中以待合适的时机将 callback function 放入 Queue 中
  4. 代码执行 3000ms 的时候 setTimeout 执行完成入队:Stack[Main], Queue[setTimeout]
  5. 代码执行 5000ms 的时候栈中代码全部完成: Stack[],Queue[SetTimeout]
  6. 开始下一个循环, Queue 队列中第一个任务出队入栈:Stack[setTimeout],Queue[]
  7. 栈中代码执行,1 会打印出来,**因此答案是 5000ms **

第三题

问打印顺序是什么样的?

// 我在 Queue 中表示为 set1
setTimeout(function () {
    console.log(1)
}, 3000)

// 我在 Queue 中表示为 set2
setTimeout(function () {
    console.log(2)
}, 1000)

for(....) // 一个执行 5000ms 的 for 循环
复制代码

分析一下代码的执行情况:

  1. 开始状态:Stack[],Queue[Main]
  2. 第一次循环将 Queue 的第一个任务放到 Stack 中: Stack[Main],Queue[]
  3. 由于 Main 中有 setTimeout 所以放到 WebAPIs 中以待合适的时机将 callback function 放入 Queue 中
  4. 代码执行 1000ms 的时候 set2 的 WebAPIs 执行完成:Stack[Main], Queue[set2]
  5. 代码执行 3000ms 的时候 set1 的 WebAPIs 执行完成: Stack[Main],Queue[set2, set1]
  6. 代码执行到 5000ms 的时候 Stack 中代码执行完成,Queue 中第一个任务出队进入 Stack 中:Stack[set2], Queue[set1]
  7. 如此执行因此答案是:2,1

扩展阅读

这里借用一下 《JavaScript 忍者秘籍》的图,如果所示这就是一个完整的 Event Loop 牵扯到的东西,可以简单理解为将之前 Queue 分为了三个部分 Task(宏任务)队列、microTask(微任务)队列、UI渲染。循环步骤如下:

  1. 将 Task 队列中的第一个 Task 压入栈执行,第一个宏任务是 Main 函数
  2. Main 函数执行完成,依次执行 microTask 中所有的任务
  3. microTask 中所有的任务执行完成进行一次 UI 渲染
  4. 再接着将 Task 队列中的第一个 Task 压入栈,如此反复执行

你可能要问了,为什么要有 Task 和 microTask 之分?这里你要考虑俩个常见的场景:

  1. Task 中的任务是一直往后排的,当你某个任务想要快速响应的时候你是做不到的,因此你用一个 microTask 就能够插队优先执行
  2. 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

答案解析:

  1. 开始状态: Stack[],Task[Main],microTask[]
  2. Task 队列第一个 Task 压入栈: Stack[Main],Task[],microTask[]
  3. 栈执行完毕: Stack[], Task[setTimeout], microTask[P1]
  4. 开始依次执行 microTask 队列中的所有任务:P1 执行,打印 2,发现新的 microTask 加入 microTask 队列: Stack[], Task[setTimeout], microTask[P2]
  5. 第一个 microTask 执行完毕,检查 microTask 队列是否还有,发现还有 P2,执行,打印 3:Stack[], Task[setTimeout], microTask[]
  6. microTask 队列中没有任务,检测是否需要 UI 渲染,一轮循环完成,将 Task 队列中的第一个 Task 压入栈:Stack[setTimeout], Task[], microTask[]
  7. 栈中代码执行完成,打印 1:Stack[], Task[], microTask[]
  8. 等待新的 Task 进入 Task 队列

总结

整个 Event Loop 过程是有迹可循的,关键是明白这个过程。并且区分出来哪些异步任务是 Task 和 microTask

我创建了一个交流群,欢迎大家扫码关注公众号进行获取。