JavaScript Event Loop 機制詳解與 Vue.js 中實踐應用概括於筆者的現代 JavaScript 開發:語法基礎與實踐技巧系列文章。本文依次介紹了函數調用棧、MacroTask 與 MicroTask 執行順序、淺析 Vue.js 中 nextTick 實現等內容;本文中引用的參考資料統一聲明在 JavaScript 學習與實踐資料索引。javascript
JavaScript 是典型的單線程單併發語言,即表示在同一時間片內其只能執行單個任務或者部分代碼片。換言之,咱們能夠認爲某個同域瀏覽器上下中 JavaScript 主線程擁有一個函數調用棧以及一個任務隊列(參考 whatwg 規範);主線程會依次執行代碼,當遇到函數時,會先將函數入棧,函數運行完畢後再將該函數出棧,直到全部代碼執行完畢。當函數調用棧爲空時,運行時即會根據事件循環(Event Loop)機制來從任務隊列中提取出待執行的回調並執行,執行的過程一樣會進行函數幀的入棧出棧操做。每一個線程有本身的事件循環,因此每一個 Web Worker有本身的,因此它才能夠獨立執行。然而,全部同屬一個 origin 的窗體都共享一個事件循環,因此它們能夠同步交流。html
Event Loop(事件循環)並非 JavaScript 中獨有的,其普遍應用於各個領域的異步編程實現中;所謂的 Event Loop 便是一系列回調函數的集合,在執行某個異步函數時,會將其回調壓入隊列中,JavaScript 引擎會在異步代碼執行完畢後開始處理其關聯的回調。java
在 Web 開發中,咱們經常會須要處理網絡請求等相對較慢的操做,若是將這些操做所有以同步阻塞方式運行無疑會大大下降用戶界面的體驗。另外一方面,咱們點擊某些按鈕以後的響應事件可能會致使界面重渲染,若是由於響應事件的執行而阻塞了界面的渲染,一樣會影響總體性能。實際開發中咱們會採用異步回調來處理這些操做,這種調用者與響應之間的解耦保證了 JavaScript 可以在等待異步操做完成以前仍然可以執行其餘的代碼。Event Loop 正是負責執行隊列中的回調而且將其壓入到函數調用棧中,其基本的代碼邏輯以下所示:git
while (queue.waitForMessage()) { queue.processNextMessage(); }
完整的瀏覽器中 JavaScript 事件循環機制圖解以下:github
在 Web 瀏覽器中,任什麼時候刻都有可能會有事件被觸發,而僅有那些設置了回調的事件會將其相關的任務壓入到任務隊列中。回調函數被調用時即會在函數調用棧中建立初始幀,而直到整個函數調用棧清空以前任何產生的任務都會被壓入到任務隊列中延後執行;順序的同步函數調用則會建立新的棧幀。總結而言,瀏覽器中的事件循環機制闡述以下:web
瀏覽器內核會在其它線程中執行異步操做,當操做完成後,將操做結果以及事先定義的回調函數放入 JavaScript 主線程的任務隊列中。面試
JavaScript 主線程會在執行棧清空後,讀取任務隊列,讀取到任務隊列中的函數後,將該函數入棧,一直運行直到執行棧清空,再次去讀取任務隊列,不斷循環。編程
當主線程阻塞時,任務隊列仍然是可以被推入任務的。這也就是爲何當頁面的 JavaScript 進程阻塞時,咱們觸發的點擊等事件,會在進程恢復後依次執行。api
在變量做用域與提高一節中咱們介紹過所謂執行上下文(Execution Context)的概念,在 JavaScript 代碼執行過程當中,咱們可能會擁有一個全局上下文,多個函數上下文或者塊上下文;每一個函數調用都會創造新的上下文與局部做用域。而這些執行上下文堆疊就造成了所謂的執行上下文棧(Execution Context Stack),便如上文介紹的 JavaScript 是單線程事件循環機制,同時刻僅會執行單個事件,而其餘事件都在所謂的執行棧中排隊等待:promise
而從 JavaScript 內存模型的角度,咱們能夠將內存劃分爲調用棧(Call Stack)、堆(Heap)以及隊列(Queue)等幾個部分:
其中的調用棧會記錄全部的函數調用信息,當咱們調用某個函數時,會將其參數與局部變量等壓入棧中;在執行完畢後,會彈出棧首的元素。而堆則存放了大量的非結構化數據,譬如程序分配的變量與對象。隊列則包含了一系列待處理的信息與相關聯的回調函數,每一個 JavaScript 運行時都必須包含一個任務隊列。當調用棧爲空時,運行時會從隊列中取出某個消息而且執行其關聯的函數(也就是建立棧幀的過程);運行時會遞歸調用函數並建立調用棧,直到函數調用棧所有清空再從任務隊列中取出消息。換言之,譬如按鈕點擊或者 HTTP 請求響應都會做爲消息存放在任務隊列中;須要注意的是,僅當這些事件的回調函數存在時纔會被放入任務隊列,不然會被直接忽略。
譬如對於以下的代碼塊:
function fire() { const result = sumSqrt(3, 4) console.log(result); } function sumSqrt(x, y) { const s1 = square(x) const s2 = square(y) const sum = s1 + s2; return Math.sqrt(sum) } function square(x) { return x * x; } fire()
其對應的函數調用圖(整理自這裏)爲:
這裏還值得一提的是,Promise.then 是異步執行的,而建立 Promise 實例 (executor) 是同步執行的,譬以下述代碼:
(function test() { setTimeout(function() {console.log(4)}, 0); new Promise(function executor(resolve) { console.log(1); for( var i=0 ; i<10000 ; i++ ) { i == 9999 && resolve(); } console.log(2); }).then(function() { console.log(5); }); console.log(3); })() // 輸出結果爲: // 1 // 2 // 3 // 5 // 4
咱們能夠參考 Promise 規範中有關於 promise.then 的部分:
promise.then(onFulfilled, onRejected) 2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1]. Here 「platform code」 means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a 「macro-task」 mechanism such as setTimeout or setImmediate, or with a 「micro-task」 mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or 「trampoline」 in which the handlers are called.
規範要求,onFulfilled 必須在執行上下文棧(Execution Context Stack) 只包含 平臺代碼(platform code) 後才能執行。平臺代碼指引擎,環境,Promise 實現代碼等。實踐上來講,這個要求保證了 onFulfilled 的異步執行(以全新的棧),在 then 被調用的這個事件循環以後。
在面試中咱們經常會碰到以下的代碼題,其主要就是考校 JavaScript 不一樣任務的執行前後順序:
// 測試代碼 console.log('main1'); // 該函數僅在 Node.js 環境下可使用 process.nextTick(function() { console.log('process.nextTick1'); }); setTimeout(function() { console.log('setTimeout'); process.nextTick(function() { console.log('process.nextTick2'); }); }, 0); new Promise(function(resolve, reject) { console.log('promise'); resolve(); }).then(function() { console.log('promise then'); }); console.log('main2'); // 執行結果 main1 promise main2 process.nextTick1 promise then setTimeout process.nextTick2
咱們在前文中已經介紹過 JavaScript 的主線程在遇到異步調用時,這些異步調用會馬上返回某個值,從而讓主線程不會在此處阻塞。而真正的異步操做會由瀏覽器執行,主線程則會在清空當前調用棧後,按照先入先出的順序讀取任務隊列裏面的任務。而 JavaScript 中的任務又分爲 MacroTask 與 MicroTask 兩種,在 ES2015 中 MacroTask 即指 Task,而 MicroTask 則是指代 Job。典型的 MacroTask 包含了 setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering 等,MicroTask 包含了 process.nextTick, Promises, Object.observe, MutationObserver 等。 兩者的關係能夠圖示以下:
參考 whatwg 規範 中的描述:一個事件循環(Event Loop)會有一個或多個任務隊列(Task Queue,又稱 Task Source),這裏的 Task Queue 就是 MacroTask Queue,而 Event Loop 僅有一個 MicroTask Queue。每一個 Task Queue 都保證本身按照回調入隊的順序依次執行,因此瀏覽器能夠從內部到JS/DOM,保證動做按序發生。而在 Task 的執行之間則會清空已有的 MicroTask 隊列,在 MacroTask 或者 MicroTask 中產生的 MicroTask 一樣會被壓入到 MicroTask 隊列中並執行。參考以下代碼:
function foo() { console.log("Start of queue"); bar(); setTimeout(function() { console.log("Middle of queue"); }, 0); Promise.resolve().then(function() { console.log("Promise resolved"); Promise.resolve().then(function() { console.log("Promise resolved again"); }); }); console.log("End of queue"); } function bar() { setTimeout(function() { console.log("Start of next queue"); }, 0); setTimeout(function() { console.log("End of next queue"); }, 0); } foo(); // 輸出 Start of queue End of queue Promise resolved Promise resolved again Start of next queue End of next queue Middle of queue
上述代碼中首個 TaskQueue 即爲 foo(),foo() 又調用了 bar() 構建了新的 TaskQueue,bar() 調用以後 foo() 又產生了 MicroTask 並被壓入了惟一的 MicroTask 隊列。咱們最後再總計下 JavaScript MacroTask 與 MicroTask 的執行順序,當執行棧(call stack)爲空的時候,開始依次執行:
把最先的任務(task A)放入任務隊列
若是 task A 爲null (那任務隊列就是空),直接跳到第6步
將 currently running task 設置爲 task A
執行 task A (也就是執行回調函數)
將 currently running task 設置爲 null 並移出 task A
執行 microtask 隊列
a: 在 microtask 中選出最先的任務 task X
b: 若是 task X 爲null (那 microtask 隊列就是空),直接跳到 g
c: 將 currently running task 設置爲 task X
d: 執行 task X
e: 將 currently running task 設置爲 null 並移出 task X
f: 在 microtask 中選出最先的任務 , 跳到 b
g: 結束 microtask 隊列
跳到第一步
在 Vue.js 中,其會異步執行 DOM 更新;當觀察到數據變化時,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據改變。若是同一個 watcher 被屢次觸發,只會一次推入到隊列中。這種在緩衝時去除重複數據對於避免沒必要要的計算和 DOM 操做上很是重要。而後,在下一個的事件循環「tick」中,Vue 刷新隊列並執行實際(已去重的)工做。Vue 在內部嘗試對異步隊列使用原生的 Promise.then
和 MutationObserver
,若是執行環境不支持,會採用 setTimeout(fn, 0)
代替。
《由於本人失誤,原來此處內容拷貝了 https://www.zhihu.com/questio... 這個回答,形成了侵權,深表歉意,已經刪除,後續我會在 github 連接上重寫本段》
而當咱們但願在數據更新以後執行某些 DOM 操做,就須要使用 nextTick
函數來添加回調:
// HTML <div id="example">{{message}}</div> // JS var vm = new Vue({ el: '#example', data: { message: '123' } }) vm.message = 'new message' // 更改數據 vm.$el.textContent === 'new message' // false Vue.nextTick(function () { vm.$el.textContent === 'new message' // true })
在組件內使用 vm.$nextTick() 實例方法特別方便,由於它不須要全局 Vue ,而且回調函數中的 this 將自動綁定到當前的 Vue 實例上:
Vue.component('example', { template: '<span>{{ message }}</span>', data: function () { return { message: '沒有更新' } }, methods: { updateMessage: function () { this.message = '更新完成' console.log(this.$el.textContent) // => '沒有更新' this.$nextTick(function () { console.log(this.$el.textContent) // => '更新完成' }) } } })
src/core/util/env
/** * 使用 MicroTask 來異步執行批次任務 */ export const nextTick = (function() { // 須要執行的回調列表 const callbacks = []; // 是否處於掛起狀態 let pending = false; // 時間函數句柄 let timerFunc; // 執行而且清空全部的回調列表 function nextTickHandler() { pending = false; const copies = callbacks.slice(0); callbacks.length = 0; for (let i = 0; i < copies.length; i++) { copies[i](); } } // nextTick 的回調會被加入到 MicroTask 隊列中,這裏咱們主要經過原生的 Promise 與 MutationObserver 實現 /* istanbul ignore if */ if (typeof Promise !== 'undefined' && isNative(Promise)) { let p = Promise.resolve(); let logError = err => { console.error(err); }; timerFunc = () => { p.then(nextTickHandler).catch(logError); // 在部分 iOS 系統下的 UIWebViews 中,Promise.then 可能並不會被清空,所以咱們須要添加額外操做以觸發 if (isIOS) setTimeout(noop); }; } else if ( typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]') ) { // 當 Promise 不可用時候使用 MutationObserver // e.g. PhantomJS IE11, iOS7, Android 4.4 let counter = 1; let observer = new MutationObserver(nextTickHandler); let textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = () => { counter = (counter + 1) % 2; textNode.data = String(counter); }; } else { // 若是都不存在,則回退使用 setTimeout /* istanbul ignore next */ timerFunc = () => { setTimeout(nextTickHandler, 0); }; } return function queueNextTick(cb?: Function, ctx?: Object) { let _resolve; callbacks.push(() => { if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); if (!pending) { pending = true; timerFunc(); } // 若是沒有傳入回調,則表示以異步方式調用 if (!cb && typeof Promise !== 'undefined') { return new Promise((resolve, reject) => { _resolve = resolve; }); } }; })();