淺析JavaScript的事件循環機制

本文爲我的看法,若是發現文章有錯誤的地方,歡迎你們指正,感謝感謝~~前端

轉載請標明出處node

本文針對於Chrome瀏覽器環境下的事件循環機制,node環境下尚未進行試驗,之後再試驗下~~

前言

衆所周知,JavaScript的一大特色就是單線程,也就是會按順序執行代碼,同一時間只能作一件事。web

爲何JavaScript會被設計成單線程?

JavaScript的誕生,一開始是爲了解決瀏覽器用戶交互的問題,以及用來操做DOM,基於這個緣由,JavaScript被設計成單線程,不然會帶來複雜的同步問題。ajax

爲何JavaScript須要異步?

單線程意味着全部任務都要排隊進行,若是存在一個任務執行時間過長,後面的任務都會被阻塞,對於用戶而言就意味着「卡死」。vim

單線程的JavaScript是怎麼執行異步代碼的呢?api

這就涉及到JavaScript的事件循環機制(event loop)了。promise

事件循環機制(event loop)

這裏先推薦去看看Philip Roberts的演講《Help, I’m stuck in an event-loop》,雖然內容沒有涉及到任務隊列的細分,可是對函數調用棧(call stack)的分析仍是挺不錯的瀏覽器

列舉幾個概念:執行上下文函數調用棧(call stack), 任務隊列(task queue)bash

  • 執行上下文(之後有空應該會再寫一篇文章分析一下哈哈):
    • 全局環境:JavaScript代碼運行起來會首先進入該環境
    • 函數環境:當函數被調用執行時,會進入當前函數中執行代碼
    • eval(不建議使用,可忽略)
  • 函數調用棧(call stack)是決定了js代碼的運行機制,遇到函數時,會生成一個新的函數上下文,而且入棧,執行完畢後出棧
  • JavaScript中的任務分爲macro-task(宏任務)與micro-task(微任務)
  • macro-task包括:script(一段代碼),setTimeout,setInterval,setImmediate,requestAnimationFrame,I/O,UI rendering
  • micro-task包括:process.nextTick, Promise, Object.observe, MutationObserver

  1. 當JavaScript代碼開始執行時,首先將全局環境壓入函數調用棧(棧底永遠都是全局上下文,除非線程結束,在瀏覽器上表現爲窗口關閉),以後,每遇到一個函數,建立一個新的函數上下文,而且入棧。異步

  2. 執行過程當中,遇到了macro-task或者micro-task,都會將其交給對應的web api去處理,好比setTimeout交給timer模塊,ajax請求交給network模塊,DOM操做交給DOM對應模塊處理,處理完成後,會將對應的回調函數放入對應的隊列中(macro-task隊列以及micro-task隊列)

  3. 每當函數調用棧中的上下文都執行完畢時(全局環境仍然存在),主進程會去查詢micro-task隊列,若是micro-task隊列爲空,會取macro-task隊列第一個task放入調用棧執行,不然,取micro-task隊列的第一個task放入調用棧執行,若是在處理task期間,若是有新添加的microtasks或者macro-task,也會被添加到相應隊列的末尾

  4. 一直循環第3步,直至全部任務執行完畢,這就是事件循環

按照個人思路大概畫了個流程圖


來實踐一下,想象如下代碼片斷的控制檯輸出

console.log('start')

setTimeout(function setTimeout1() {
    console.log('setTimeout1')
    setTimeout(function setTimeout3() {
        console.log('setTimeout3')
        new Promise(function promise4(resolve, reject) {
            console.log('promise4')
            resolve('then')
        }).then(function then4() {
            console.log('promise4 then')
        })
    }, 0)

    new Promise(function promise3(resolve, reject) {
        console.log('promise3')
        setTimeout(function setTimeout4() {
            resolve('then')
        }, 0)
        console.log('after resolve')
    }).then(function then3() {
        console.log('promise3 then')
    })

}, 0)

new Promise(function promise1(resolve, reject) {
    console.log('promise1')
    resolve('then')
}).then(function then1() {
    console.log('promise1 then')
    new Promise(function promise2(resolve, reject) {
        console.log('promise2')
        resolve('then')
    }).then(function then2() {
        console.log('promise2 then')
    })
})

setTimeout(function setTimeout2() {
    console.log('setTimeout2')
}, 0)



console.log('end')


/* 
控制檯輸出
start
promise1
end
promise1 then
promise2
promise2 then
setTimeout1
promise3
after resolve
setTimeout2
setTimeout3
promise3 then
*/

複製代碼

細化步驟還挺多的,因此作了個gif~~

第一步

全局上下文global進棧

全局上下文global進棧

第二步

console.log('start')
複製代碼

遇到console.log,函數進棧,調用web api的console接口,運行完成後出棧

第三步

setTimeout(function setTimeout1() {
 //....
}, 0)
複製代碼

遇到setTimeout,交給timer模塊執行,setTimeout出棧,timer執行完該定時器後(0秒後),將回調函數setTimeout1放入macro-task隊尾。劃重點!!這是一個很容易產生誤解的地方,不少同窗下意識都以爲定時器就是到了設定時間後當即執行,實際上是到了時間後,將回調函數放入macro-task隊列,等待執行

第四步

new Promise(function promise1(resolve, reject) {
    console.log('promise1')
    resolve('then')
}).then(function then1() {
    //...
})
複製代碼

遇到promise,構造函數裏的promise1會馬上進棧而且執行,執行中遇到了resolve函數,進棧,將回調函數then1放入micro-task隊列,此時promise1resolve都已執行完畢,出棧

第五步

setTimeout(function setTimeout2() {
    //...
}, 0)
複製代碼

遇到setTimeout,交給timer模塊執行,setTimeout出棧,timer執行完該定時器後(0秒後),將回調函數setTimeout2放入macro-task隊尾。

第六步

console.log('end')
複製代碼

第七步

到了很關鍵的一步,這個時候call stack已經執行完了(只剩下global),主進程會去查詢micro-task隊列,發現裏面有等待執行的函數,取隊首的函數(也就是then1)進棧執行

function then1() {
    console.log('promise1 then')
    new Promise(function promise2(resolve, reject) {
        console.log('promise2')
        resolve('then')
    }).then(function then2() {
        //...
    })
}
複製代碼

在執行過程當中,又遇到了promise,先執行構造函數裏的promise2,執行中遇到了resolve函數,進棧,將回調函數then2放入micro-task隊列,此時then1promise2resolve都已執行完畢,出棧

第八步

call stack執行完畢,查詢micro-task隊列,發現裏面有等待執行的函數,取隊首的函數(也就是then2)進棧執行

function then2() {
    console.log('promise2 then')
}
複製代碼

第九步

call stack執行完畢,查詢micro-task隊列,發現爲空,查詢macro-task隊列,發現裏面有等待執行的函數,取隊首的函數(也就是setTimeout1)進棧執行

function setTimeout1() {
    console.log('setTimeout1')
    setTimeout(function setTimeout3() {
      //...
    }, 0)

    new Promise(function promise3(resolve, reject) {
        console.log('promise3')
        setTimeout(function setTimeout4() {
            //...
        }, 0)
        console.log('after resolve')
    }).then(function then3() {
        //...
    })
}
複製代碼

執行中遇到setTimeout,交給timer模塊執行,setTimeout出棧,timer執行完該定時器後(0秒後),將回調函數setTimeout3放入macro-task隊尾。 繼續執行,遇到了promise,先執行構造函數裏的promise3,又遇到了setTimeout,交給timer模塊執行,setTimeout出棧,timer執行完該定時器後(0秒後),將回調函數setTimeout4放入macro-task隊尾,此時setTimeout1promise3 都已執行完畢,出棧

第十步

call stack執行完畢,查詢micro-task隊列,發現爲空,查詢macro-task隊列,發現裏面有等待執行的函數,取隊首的函數(也就是setTimeout2)進棧執行

function setTimeout2() {
    console.log('setTimeout2')
}
複製代碼

第十步

call stack執行完畢,查詢micro-task隊列,發現爲空,查詢macro-task隊列,發現裏面有等待執行的函數,取隊首的函數(也就是setTimeout3)進棧執行

function setTimeout3() {
    console.log('setTimeout3')
    new Promise(function promise4(resolve, reject) {
        console.log('promise4')
        resolve('then')
    }).then(function then4() {
        console.log('promise4 then')
    })
}
複製代碼

執行中遇到了promise,先執行構造函數裏的promise4,遇到了resolve函數,進棧,將回調函數then2放入micro-task隊列,此時setTimeout3promise4都已執行完畢,出棧

第十一步

call stack執行完畢,查詢micro-task隊列,發現裏面有等待執行的函數,取隊首的函數(也就是then4)進棧執行

function then4() {
    console.log('promise4 then')
}
複製代碼

第十二步

call stack執行完畢,查詢micro-task隊列,發現爲空,查詢macro-task隊列,發現裏面有等待執行的函數,取隊首的函數(也就是setTimeout4)進棧執行

function setTimeout4() {
    resolve('then')
}
複製代碼

執行遇到resolve,將promise的回調函數then3放入micro-task隊列,此時setTimeout4resolve已執行完畢,出棧

第十三步

call stack執行完畢,查詢micro-task隊列,發現裏面有等待執行的函數,取隊首的函數(也就是then3)進棧執行,執行完畢後出棧,至此所有代碼執行完畢

呼~終於寫完了

總結

  1. 主進程開始執行代碼時,先將全局環境入棧,之後每遇到一個函數,建立一個新的上下文,進棧而且執行,遇到主進程執行不了的函數,交給web api執行,同時出棧
  2. 執行過程當中,遇到了macro-task或者micro-task,都會將其交給對應的web api去處理,好比setTimeout交給timer模塊,ajax請求交給network模塊,DOM操做交給DOM對應模塊處理,處理完成後,會將對應的回調函數放入對應的隊列中(macro-task隊列以及micro-task隊列)
  3. 每當函數調用棧中的上下文都執行完畢時(全局環境仍然存在),主進程會去查詢micro-task隊列,若是micro-task隊列爲空,會取macro-task隊列第一個task放入調用棧執行,不然,取micro-task隊列的第一個task放入調用棧執行,若是在處理task期間,若是有新添加的microtasks或者macro-task,也會被添加到相應隊列的末尾
  4. 以上內容只針對於Chrome瀏覽器環境,node環境尚未具體測試,好像是不太同樣的

關於

前端萌新一個~~打算常常寫寫文章總結一下知識點,歡迎關注,一塊兒加油啦

相關文章
相關標籤/搜索