美文网首页
JavaScript 的并发模型

JavaScript 的并发模型

作者: 柏丘君 | 来源:发表于2018-01-16 10:15 被阅读0次

    JavaScript 是单线程的,这意味着在任何时候只能有一段代码执行。JavaScript 主线程在运行时,会建立一个执行同步代码的栈和执行异步代码的队列,如下图所示:


    JavaScript主线程的栈和队列.png

    异步代码的执行时机

    JavaScript 主线程在执行时,如果遇到异步的代码,就会将这些代码加入到异步队列中,然后继续执行同步代码栈中的代码。
    这里需要注意的是,下面的代码表示的是在 5 秒后被加入异步队列等待执行,而不是加入异步队列 5 秒后执行:

    setTimeout(() => console.log("1"),5);
    

    当同步代码栈被清空后,意味着同步代码已经执行完毕,这时就开始执行异步队列中代码。
    异步队列中的代码在执行时,会将其的回调函数和相关的函数调用放到同步代码栈中去执行。当同步代码栈被清空,意味着当前的异步任务已经执行完毕,然后从异步队列中取下一个任务执行,循环往复。
    以上就是一个基本的 JavaScript 并发模型。

    一段经典的代码

    以下是一段面试中经常考察的一段经典代码,你可以试着推敲一下打印结果:

    // 请写出下面代码的输出结果
    setTimeout(() => {
        console.log(1)
    },0);
    
    console.log(2);
    
    new Promise((res) => {
        console.log(3)
        res()
        console.log(4)
    }).then(()=>{
        console.log(5)
    }).then(() => {
        console.log(6)
    })
    
    console.log(7)
    

    这段代码的正确打印结果为:

    2
    3
    4
    7
    5
    6
    1
    

    从上面的输出结果大概可以看出:同步代码总是优先于异步代码执行的,而对于异步代码的执行,似乎有个优先顺序,这其实和异步队列的实现有关,我们可以再深入了解一下。

    Macro Tasks 和 Micro Tasks

    在上面的打印结果中,Promise 在 resolve 后先于 setTimeout 执行,说明 Promise 任务的优先级比 setTimeout 任务的优先级要高。这就引出了两个概念:Macro Tasks(宏任务)和 Micro Tasks(微任务)。
    JavaScript 的异步队列实际上又被划分为两个小队列:宏任务队列和微任务队列。宏任务队列中包含了以下异步任务:

    • setTimeout
    • setInterval
    • setImmediate
    • UI 事件
    • Ajax

    微任务队列中包含了以下异步任务:

    • Promise
    • process.nextTick
    • Object.observe(已废弃)
    • MutationObserver(可用来监听 DOM 变化)

    一般来说,宏任务的开销比微任务的开销要大。
    它们的关系如图所示:


    异步队列.png

    Macro Tasks 和 Micro Tasks 的执行顺序

    Macro Tasks 和 Micro Tasks 的执行顺序如下:在执行每个 Macro Task(宏任务)之前,会先检查有微任务队列中有没有任务需要处理,若有,就先将微任务队列中的任务全部放入同步执行栈中执行,直到微任务队列被清空,然后再执行宏任务队列中的任务,循环往复。
    因此,这就能解释为什么 Promise 会优先于 setTimeout 执行,即使 Promise 的执行链很长。这是因为 setTimeout 属于 Macro Task,而 Promise 属于 Micro Task,在执行 Macro Task 之前需要先将 Micro Task 队列清空。如下面的代码:

    setTimeout(()=>{
        console.log(1)
    },0)
    
    new Promise((res) => {
        res()
    }).then(() => {
        console.log(4)
    }).then(() =>{
        console.log(5)
    }).then(() =>{
        console.log(6)
    }).then(() =>{
        console.log(7)
    }).then(() =>{
        console.log(8)
    }).then(() =>{
        console.log(9)
    }).then(() =>{
        console.log(10)
    })
    

    输出结果为:

    4
    5
    6
    7
    8
    9
    10
    1
    

    需要注意的是:新建 Promise 对象时需要传入一个函数参数,这个函数参数是同步代码,如下例子:

    new Promise(() => {
        console.log(1)
    });
    
    console.log(2)
    

    打印结果为:

    1
    2
    

    总结

    本文简单介绍了 JavaScript 的并发模型,或者说 JavaScript 代码的执行顺序。这里做一个总结:

    1. JavaScript 主线程在运行时,如果发现异步方法,会将它们放入异步队列中,同步方法则放入同步执行栈中依次执行
    2. 异步队列中的代码只有在同步执行栈被清空后才有机会执行
    3. 异步队列又分为 Macro Tasks 队列(宏任务队列)和 Micro Tasks 队列(微任务队列),在执行每一个 Macro Task 之前,总是会先执行 Micro Tasks 队列中的代码(若有),当 Micro Tasks 被清空之后,再去执行 Macro Task。

    附:参考资料
    JavaScript并发模型与Event Loop
    HTML系列:macrotask和microtask
    MutationObserver
    Navigator.sendBeacon

    完。

    相关文章

      网友评论

          本文标题:JavaScript 的并发模型

          本文链接:https://www.haomeiwen.com/subject/sgtdoxtx.html