Event Loop - JS執行機制

依然是:經濟基礎決定上層建築。javascript


說明

  • 首先,旨在搞清經常使用的同步異步執行機制
  • 其次,暫時不討論node.js的Event Loop執行機制,如下關於瀏覽器的Event Loop執行機制
  • 最後,借鑑了不少前輩的研究文章,很是感謝,此文主要是梳理所學,還請保持質疑以追求正確的知識

要點

  • 基本概念
  • 同步異步操做
  • Event Loop

基本概念

先解釋現代js引擎幾個概念。html

runtime

  • stack(棧):這裏放着js正在執行的任務。理解事件循環一(淺析)一文有對 stack 的 example 解釋。
  • heap(堆):一個用來表示內存中一大片非結構化區域的名字,對象都被分配在這。
  • queue(隊列):一個 js runtime 包含了一個任務隊列,該隊列是由一系列待處理的任務組成。而每一個任務都有相對應的函數。當棧爲空時,就會從任務隊列中取出一個任務,並處理之。當該任務處理完畢後,棧就會再次爲空。(queue的特色是先進先出(FIFO))。

爲了方便描述與理解,做出如下約定:html5

  • stack 棧爲主線程
  • queue 隊列爲任務隊列(等待調度到主線程執行)

同步異步

js 是一門單線程語言。 js 引擎有一個主線程(main thread)用來解釋和執行 js 程序,實際上還存在其餘的線程。例如:處理AJAX請求的線程、處理DOM事件的線程、定時器線程、讀寫文件的線程(例如在node.js中)等等。這些線程可能存在於 js 引擎以內,也可能存在於 js 引擎以外,在此咱們不作區分。不妨叫它們工做線程。可是前輩們很有一種小本本記好的說法,那就是,要相信 js 單線程的本質,其餘一切看似多線程,都是紙老虎。哈哈哈哈哈哈哈哈哈哈哈哈哈......java

任務分爲同步任務(synchronous)和異步任務(asynchronous),若是全部任務都由主線程來處理,會出現主線程被阻塞而使得頁面「假死」。爲了主線程不被阻塞,異步任務(如:AJAX異步請求,定時器等)就會交給工做線程來處理,異步任務完成後將異步回調函數註冊進任務隊列,等待主線程空閒時調用。流程如圖:node

同步異步

// example
console.log('example-start')

setTimeout(() => {
  console.log('setTimeout-0')
}, 0)

console.log('example-end')

/* chrome result
 * 
    example-start
    example-end
    setTimeout-0
 *
 */

上面一個簡單的小 js 片斷的執行過程:git

  • 主線程開始同步任務執行,執行console.log('example-start')
  • 而後接下來,主線程碰見一個異步操做setTimeout,將改異步任務交給工做線程處理,異步任務完成以後,將回調函數註冊進任務隊列,等待被調用
  • 繼續同步任務處理,執行console.log('example-end')
  • 主線程空閒,調用任務隊列中等待執行的回調函數,執行console.log('setTimeout-0')

最後借用Philip Roberts的生動形象的一張圖,callback queue能夠簡單理解爲任務隊列,詳細的下面會講。github

chrome

Event Loop

然而Event Loop並無上面圖中描述那麼簡單。心塞塞 : (web

根據規範,事件循環是經過任務隊列的機制來進行協調的。一個 Event Loop 中,能夠有一個或者多個任務隊列(task queue),一個任務隊列即是一系列有序任務(task)的集合;每一個任務都有一個任務源(task source),源自同一個任務源的 task 必須放到同一個任務隊列,從不一樣源來的則被添加到不一樣隊列。chrome

setTimeout/Promise 等API即是任務源,而進入任務隊列的是他們指定的具體執行任務(回調函數)。來自不一樣任務源的任務會進入到不一樣的任務隊列。其中setTimeout與setInterval是同源的。vim

仔細查閱規範可知,異步任務可分爲 task(部分文章也稱爲 macro-task) 和 micro-task 兩類,不一樣的API註冊的異步任務會依次進入自身對應的隊列中,而後等待 Event Loop 將它們依次壓入執行棧中執行。

  • task主要包含:script(總體代碼)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(node.js 環境)
  • micro-task主要包含:Promise.then、MutaionObserver、MessageChannel、process.nextTick(node.js 環境)

在事件循環中,每進行一次循環操做稱爲 tick,每一次 tick 的任務處理模型是比較複雜的,但關鍵步驟以下:

  • 在這次 tick 中選擇最早進入隊列的任務(oldest task),若是有則執行(一次)
  • 檢查是否存在 micro-task,若是存在則不停地執行,直至清空 micro-task queue
  • 更新 render
  • 主線程重複執行上述步驟

Event Loop

一個事件循環(Event Loop)中,主線程從任務隊列中取出一個任務 task 執行時,而這個正在執行的任務就是從 task queue(部分文章也稱爲 macro-task queue)中來的。當這個 task 執行結束後,js 會將 micro-task queue中全部 micro-task 都在同一個 Event Loop 中執行,當這些 micro-task 執行結束後還能繼續添加 micro-task 一直到整個 micro-task 隊列執行結束。而後當前本輪的 Event Loop 結束,主線程能夠繼續取下一個 task 執行。因此更詳細的 Event Loop 的流程圖以下:

Event Loop

// example
console.log('example-start')

setTimeout(() => {
  console.log('setTimeout-0') // setTimeout-1
}, 0)

new Promise((resolve, reject) => {
  console.log('promise-1')
  resolve('promise-2')
  Promise.resolve().then(() => console.log('promise-3')) // then-1
}).then((response) => { // then-2
  console.log(response)
  setTimeout(() => {
    console.log('setTimeout-10') // setTimeout-2
  }, 10)
})

console.log('example-end')

/* chrome result
 * 
    example-start
    promise-1
    example-end
    promise-3
    promise-2
    setTimeout-0
    setTimeout-10
 *
 */

上面一個簡單的 js 片斷的執行過程:

  • 第一輪事件循環:
    第一輪事件循環
  • 第二輪事件循環:
    第二輪事件循環
  • 第三輪事件循環:
    第三輪事件循環

若是上文理解有誤或者有疑惑,歡迎交流。

參考


好記性不如爛筆頭。

相關文章
相關標籤/搜索