🧑💻 寫在開頭
點贊 + 收藏 === 學會🤣🤣🤣
原始碼實現思路(面試高分回答) 📖
面試官問我 Vue 的 nextTick
原理是怎麼實現的,我這樣回答:
在呼叫 this.$nextTick(cb)
之前:
- 存在一個
callbacks
陣列,用於存放所有的cb
回撥函式。 - 存在一個
flushCallbacks
函式,用於執行callbacks
陣列中的所有回撥函式。 - 存在一個
timerFunc
函式,用於將flushCallbacks
函式新增到任務佇列中。
當呼叫 this.nextTick(cb)
時:
nextTick
會將cb
回撥函式新增到callbacks
陣列中。- 判斷在當前事件迴圈中是否是第一次呼叫
nextTick
:- 如果是第一次呼叫,將執行
timerFunc
函式,新增flushCallbacks
到任務佇列。 - 如果不是第一次呼叫,直接下一步。
- 如果是第一次呼叫,將執行
- 如果沒有傳遞
cb
回撥函式,則返回一個 Promise 例項。
根據上述描述,對應的`流程圖`如下:
如果上面的描述沒有很理解。沒關係,花幾分鐘跟著我下面來,看完下面的原始碼逐行講解,你一定能夠清晰地向別人講出你的思路!
nextTick思路詳解 🏃♂➡
1. 核心程式碼 🌟
下面用十幾行程式碼,就已經可以基本實現nextTick的功能(預設瀏覽器支援Promise)
// 儲存所有的cb回撥函式 const callbacks = []; /*類似於節流的標記位,標記是否處於節流狀態。防止重複推送任務*/ let pending = false; /*遍歷執行陣列 callbacks 中的所有儲存的cb回撥函式*/ function flushCallbacks() { // 重置標記,允許下一個 nextTick 呼叫 pending = false; /*執行所有cb回撥函式*/ for (let i = 0; i < callbacks.length; i++) { callbacks[i](); } // 清空回撥陣列,為下一次呼叫做準備 callbacks.length = 0; } function nextTick(cb) { // 將回撥函式cb新增到 callbacks 陣列中 callbacks.push(() => { cb(); }); // 第一次使用 nextTick 時,pending 為 false,下面的程式碼才會執行 if (!pending) { // 改變標記位的值,如果有flushCallbacks被推送到任務佇列中去則不需要重複推送 pending = true; // 使用 Promise 機制,將 flushCallbacks 推送到任務佇列 Promise.resolve().then(flushCallbacks); } }
測試一下:
let message = '初始訊息'; nextTick(() => { message = '更新後的訊息'; console.log('回撥:', message); // 輸出2: 更新後的訊息 }); console.log('測試開始:', message); // 輸出1: 初始訊息
如果你想要應付面試官,能手寫這部分核心原理就已經差不多啦。
如果你想徹底掌握它,請繼續跟著我來!!!🕵🏻♂
2. nextTick() 返回promise 🌟
我們在開發中,會使用await this.$nextTick();讓其下面的程式碼全部變成非同步程式碼。 比如寫成這樣
await this.$nextTick(); ...... ...... // 或者 this.$nextTick().then(()=>{ ...... })
核心就是nextTick()如果沒有引數,則返回一個promise
const callbacks = []; let pending = false; function flushCallbacks() { pending = false; for (let i = 0; i < callbacks.length; i++) { callbacks[i](); } callbacks.length = 0; } function nextTick(cb) { // 用於儲存 Promise 的resolve函式 let _resolve; callbacks.push(() => { /* ------------------ 新增start ------------------ */ // 如果有cb回撥函式,將cb儲存到callbacks if (cb) { cb(); } else if (_resolve) { // 如果引數cb不存在,則儲存promise的的成功回撥resolve _resolve(); } /* ------------------ 新增end ------------------ */ }); if (!pending) { pending = true; Promise.resolve().then(flushCallbacks); } /* ------------------ 新增start ------------------ */ if (!cb) { return new Promise((resolve, reject) => { // 儲存resolve到callbacks陣列中 _resolve = resolve; }); } /* ------------------ 新增end ------------------ */ }
測試一下:
async function testNextTick() { let message = "初始訊息"; nextTick(() => { message = "更新後的訊息"; }); console.log("傳入回撥:", message); // 輸出1: 初始訊息 // 不傳入回撥的情況 await nextTick(); // nextTick 返回 Promise console.log("未傳入回撥後:", message); // 輸出2: 更新後的訊息 } // 執行測試 testNextTick();
3. 判斷瀏覽器環境 🔧
為了防止瀏覽器不支援 Promise,Vue 選擇了多種 API 來實現相容 nextTick:
Promise --> MutationObserver --> setImmediate --> setTimeout
-
Promise (微任務):
如果當前環境支援 Promise,Vue 會使用Promise.resolve().then(flushCallbacks)
-
MutationObserver (微任務):
如果不支援 Promise,支援 MutationObserver。Vue 會建立一個 MutationObserver 例項,透過監聽文字節點的變化來觸發執行回撥函式。 -
setImmediate (宏任務):
如果前兩者都不支援,支援 setImmediate。則:setImmediate(flushCallbacks)
注意
:setImmediate 在絕大多數瀏覽器中不被支援,但在 Node.js 中是可用的。 -
setTimeout (宏任務):
如果前面所有的都不支援,那你的瀏覽器一定支援 setTimeout!!!
終極方案:setTimeout(flushCallbacks, 0)
// 儲存所有的回撥函式 const callbacks = []; /* 類似於節流的標記位,標記是否處於節流狀態。防止重複推送任務 */ let pending = false; /* 遍歷執行陣列 callbacks 中的所有儲存的 cb 回撥函式 */ function flushCallbacks() { // 重置標記,允許下一個 nextTick 呼叫 pending = false; /* 執行所有 cb 回撥函式 */ for (let i = 0; i < callbacks.length; i++) { callbacks[i](); // 依次呼叫儲存的回撥函式 } // 清空回撥陣列,為下一次呼叫做準備 callbacks.length = 0; } // 判斷最終支援的 API:Promise / MutationObserver / setImmediate / setTimeout let timerFunc; if (typeof Promise !== "undefined") { // 建立一個已resolve的 Promise 例項 var p = Promise.resolve(); // 定義 timerFunc 為使用 Promise 的方式排程 flushCallbacks timerFunc = () => { // 使用 p.then 方法將 flushCallbacks 推送到微任務佇列 p.then(flushCallbacks); }; } else if ( typeof MutationObserver !== "undefined" && MutationObserver.toString() === "[object MutationObserverConstructor]" ) { /* 新建一個 textNode 的 DOM 物件,用 MutationObserver 繫結該 DOM 並指定回撥函式。 在 DOM 變化的時候則會觸發回撥,該回撥會進入主執行緒(比任務佇列優先執行), 即 textNode.data = String(counter) 時便會加入該回撥 */ var counter = 1; // 用於切換文字節點的值 var observer = new MutationObserver(flushCallbacks); // 建立 MutationObserver 例項 var textNode = document.createTextNode(String(counter)); // 建立文字節點 observer.observe(textNode, { characterData: true, // 監聽文字節點的變化 }); // 定義 timerFunc 為使用 MutationObserver 的方式排程 flushCallbacks timerFunc = () => { counter = (counter + 1) % 2; // 切換 counter 的值(0 或 1) textNode.data = String(counter); // 更新文字節點以觸發觀察者 }; } else if (typeof setImmediate !== "undefined") { /* 使用 setImmediate 將回撥推入任務佇列尾部 */ timerFunc = () => { setImmediate(flushCallbacks); // 將 flushCallbacks 推送到宏任務佇列 }; } else { /* 使用 setTimeout 將回撥推入任務佇列尾部 */ timerFunc = () => { setTimeout(flushCallbacks, 0); // 將 flushCallbacks 推送到宏任務佇列 }; } function nextTick(cb) { // 用於儲存 Promise 的解析函式 let _resolve; // 將回撥函式 cb 新增到 callbacks 陣列中 callbacks.push(() => { // 如果有 cb 回撥函式,將 cb 儲存到 callbacks if (cb) { cb(); } else if (_resolve) { // 如果引數 cb 不存在,則儲存 Promise 的成功回撥 resolve _resolve(); } }); // 第一次使用 nextTick 時,pending 為 false,下面的程式碼才會執行 if (!pending) { // 改變標記位的值,如果有 nextTickHandler 被推送到任務佇列中去則不需要重複推送 pending = true; // 呼叫 timerFunc,將 flushCallbacks 推送到合適的任務佇列 timerFunc(flushCallbacks); } // 如果沒有 cb 且環境支援 Promise,則返回一個 Promise if (!cb && typeof Promise !== "undefined") { return new Promise((resolve) => { // 儲存 resolve 到 callbacks 陣列中 _resolve = resolve; }); } }
你真的太牛了,居然幾乎全部看完了!
Vue純原始碼
上面的程式碼實現,對於 nextTick 功能已經非常完整了,接下來我將給你展示出 Vue 中實現 nextTick 的完整原始碼。無非是加了一些判斷變數是否存在的判斷。看完上面的講解,我相信聰明的你一定能理解 Vue 實現 nextTick 的原始碼了吧!💡
// 儲存所有的 cb 回撥函式 const callbacks = []; /* 類似於節流的標記位,標記是否處於節流狀態。防止重複推送任務 */ let pending = false; /* 遍歷執行陣列 callbacks 中的所有儲存的 cb 回撥函式 */ function flushCallbacks() { pending = false; // 重置標記,允許下一個 nextTick 呼叫 const copies = callbacks.slice(0); // 複製當前的 callbacks 陣列 callbacks.length = 0; // 清空 callbacks 陣列 for (let i = 0; i < copies.length; i++) { copies[i](); // 執行每一個儲存的回撥函式 } } // 判斷是否為原生實現的函式 function isNative(Ctor) { // 如Promise.toString() 為 'function Promise() { [native code] }' return typeof Ctor === "function" && /native code/.test(Ctor.toString()); } // 判斷最終支援的 API:Promise / MutationObserver / setImmediate / setTimeout let timerFunc; if (typeof Promise !== "undefined" && isNative(Promise)) { const p = Promise.resolve(); // 建立一個已解決的 Promise 例項 timerFunc = () => { p.then(flushCallbacks); // 使用 p.then 將 flushCallbacks 推送到微任務佇列 // 在某些有問題的 UIWebView 中,Promise.then 並不會完全失效, // 但可能會陷入一種奇怪的狀態:回撥函式被新增到微任務佇列中, // 但佇列並沒有被執行,直到瀏覽器需要處理其他工作,比如定時器。 // 因此,我們可以透過新增一個空的定時器來“強制”執行微任務佇列。 if (isIOS) setTimeout(() => {}); // 解決iOS 的bug,推遲 空函式 的執行(如果不理解,建議忽略) }; } else if ( typeof MutationObserver !== "undefined" && (isNative(MutationObserver) || MutationObserver.toString() === "[object MutationObserverConstructor]") ) { let counter = 1; // 用於切換文字節點的值 const observer = new MutationObserver(flushCallbacks); // 建立 MutationObserver 例項 const textNode = document.createTextNode(String(counter)); // 建立文字節點 observer.observe(textNode, { characterData: true, // 監聽文字節點的變化 }); // 定義 timerFunc 為使用 MutationObserver 的方式排程 flushCallbacks timerFunc = () => { counter = (counter + 1) % 2; // 切換 counter 的值(0 或 1) textNode.data = String(counter); // 更新文字節點以觸發觀察者 }; } else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) { timerFunc = () => { setImmediate(flushCallbacks); // 使用 setImmediate 推送到任務佇列 }; } else { timerFunc = () => { setTimeout(flushCallbacks, 0); // 使用 setTimeout 推送到宏任務佇列 }; } function nextTick(cb, ctx) { let _resolve; // 用於儲存 Promise 的解析函式 // 將回撥函式 cb 新增到 callbacks 陣列中 callbacks.push(() => { if (cb) { try { cb.call(ctx); // 執行傳入的回撥函式 } catch (e) { handleError(e, ctx, "nextTick"); // 錯誤處理 } } else if (_resolve) { _resolve(ctx); // 解析 Promise } }); // 第一次使用 nextTick 時,pending 為 false,下面的程式碼才會執行 if (!pending) { pending = true; // 改變標記位的值 timerFunc(); // 呼叫 timerFunc,排程 flushCallbacks } // 如果沒有 cb 且環境支援 Promise,則返回一個 Promise if (!cb && typeof Promise !== "undefined") { return new Promise((resolve) => { _resolve = resolve; // 儲存解析函式 }); } }
總結
透過這樣分成三步、循序漸進的方式,我們深入探討了 nextTick 的原理和實現機制。希望這篇文章能夠對你有所幫助,讓你在前端開發的道路上更加得心應手!🚀
本文轉載於:https://juejin.cn/post/7433439452662333466
如果對您有所幫助,歡迎您點個關注,我會定時更新技術文件,大家一起討論學習,一起進步。