您好,登錄后才能下訂單哦!
今天就跟大家聊聊有關如何從Javascript事件循環看出Vue.nextTick的原理和執行機制,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結了以下內容,希望大家根據這篇文章可以有所收獲。
拋磚引玉
Vue 的特點之一就是響應式,但是有些時候數據更新了,我們看到頁面上的 DOM 并沒有立刻更新。如果我們需要在 DOM 更新之后再執行一段代碼時,可以借助 nextTick 實現。
我們先來看一個例子
export default { data() { return { msg: 0 } }, mounted() { this.msg = 1 this.msg = 2 this.msg = 3 }, watch: { msg() { console.log(this.msg) } } }
這里的結果是只輸出一個 3,而非依次輸出 1,2,3。這是為什么呢?
vue 的官方文檔是這樣解釋的:
Vue 異步執行 DOM 更新。只要觀察到數據變化,Vue 將開啟一個隊列,并緩沖在同一事件循環中發生的所有數據改變。如果同一個watcher 被多次觸發,只會被推入到隊列中一次。這種在緩沖時去除重復數據對于避免不必要的計算和 DOM 操作上非常重要。然后,在下一個的事件循環“tick”中,Vue 刷新隊列并執行實際 (已去重的) 工作。Vue 在內部嘗試對異步隊列使用原生的Promise.then和 MessageChannel,如果執行環境不支持,會采用setTimeout(fn, 0)代替。 |
假如有這樣一種情況,mounted鉤子函數下一個變量 a 的值會被++循環執行 1000 次。每次++時,都會根據響應式觸發setter->Dep->Watcher->update->run。如果這時候沒有異步更新視圖,那么每次++都會直接操作 DOM 一次,這是非常消耗性能的。所以 Vue 實現了一個queue隊列,在下一個 Tick(或者是當前 Tick 的微任務階段)的時候會統一執行queue中Watcher的run。同時,擁有相同 id 的Watcher不會被重復加入到該queue中去,所以不會執行 1000 次Watcher的run。最終的結果是直接把 a 的值從 1 變成 1000,大大提升了性能。
在 vue 中,數據監測都是通過Object.defineProperty來重寫里面的 set 和 get 方法實現的,vue 更新 DOM 是異步的,每當觀察到數據變化時,vue 就開始一個隊列,將同一事件循環內所有的數據變化緩存起來,等到下一次 eventLoop,將會把隊列清空,進行 DOM 更新。
想要了解 vue.nextTick 的執行機制,我們先來了解一下 javascript 的事件循環。
js 事件循環
js 的任務隊列分為同步任務和異步任務,所有的同步任務都是在主線程里執行的。異步任務可能會在 macrotask 或者 microtask 里面,異步任務進入 Event Table 并注冊函數。當指定的事情完成時,Event Table 會將這個函數移入 Event Queue。主線程內的任務執行完畢為空,會去 Event Queue 讀取對應的函數,進入主線程執行。上述過程會不斷重復,也就是常說的 Event Loop(事件循環)。
1. macro-task(宏任務):
每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調并放到執行棧中執行)。瀏覽器為了能夠使得 js 內部(macro)task與 DOM 任務能夠有序執行,會在一個(macro)task執行結束后,在下一個(macro)task執行開始前,對頁面進行重新渲染。宏任務主要包含:
script(整體代碼)
setTimeout / setInterval
setImmediate(Node.js 環境)
I/O
UI render
postMessage
MessageChannel
2. micro-task(微任務):
可以理解是在當前 task 執行結束后立即執行的任務。也就是說,在當前 task 任務后,下一個 task 之前,在渲染之前。所以它的響應速度相比 setTimeout(setTimeout 是 task)會更快,因為無需等渲染。也就是說,在某一個 macrotask 執行完后,就會將在它執行期間產生的所有 microtask 都執行完畢(在渲染前)。microtask 主要包含:
process.nextTick(Node.js 環境)
Promise
Async/Await
MutationObserver(html5 新特性)
3. 小結
先執行主線程
遇到宏隊列(macrotask)放到宏隊列(macrotask)
遇到微隊列(microtask)放到微隊列(microtask)
主線程執行完畢
執行微隊列(microtask),微隊列(microtask)執行完畢
執行一次宏隊列(macrotask)中的一個任務,執行完畢
執行微隊列(microtask),執行完畢
依次循環。。。
Vue.nextTick 源碼
vue 是采用雙向數據綁定的方法驅動數據更新的,雖然這樣能避免直接操作 DOM,提高了性能,但有時我們也不可避免需要操作 DOM,這時就該 Vue.nextTick(callback)出場了,它接受一個回調函數,在 DOM 更新完成后,這個回調函數就會被調用。不管是 vue.nextTick 還是vue.prototype.\$nextTick 都是直接用的nextTick這個閉包函數。
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]() } } ... })()
使用數組callbacks保存回調函數,pending表示當前狀態,使用函數nextTickHandler 來執行回調隊列。在該方法內,先通過slice(0)保存了回調隊列的一個副本,通過設置 callbacks.length = 0清空回調隊列,最后使用循環執行在副本里的所有函數。
if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve() var logError = err => { console.error(err) } timerFunc = () => { p.then(nextTickHandler).catch(logError) if (isIOS) setTimeout(noop) } } else if (typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')) { var counter = 1 var observer = new MutationObserver(nextTickHandler) var textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } } else { timeFunc = () => {
隊列控制的最佳選擇是microtask,而microtask的最佳選擇是Promise。但如果當前環境不支持 Promise,就檢測到瀏覽器是否支持 MO,是則創建一個文本節點,監聽這個文本節點的改動事件,以此來觸發nextTickHandler(也就是 DOM 更新完畢回調)的執行。此外因為兼容性問題,vue 不得不做了microtask向macrotask 的降級方案。
為讓這個回調函數延遲執行,vue 優先用promise來實現,其次是 html5 的 MutationObserver,然后是setTimeout。前兩者屬于microtask,后一個屬于 macrotask。下面來看最后一部分。
return function queueNextTick(cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) cb.call(ctx) if (_resolve) _resolve(ctx) }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
這就是我們真正調用的nextTick函數,在一個event loop內它會將調用 nextTick的cb 回調函數都放入 callbacks 中,pending 用于判斷是否有隊列正在執行回調,例如有可能在 nextTick 中還有一個 nextTick,此時就應該屬于下一個循環了。最后幾行代碼是 promise 化,可以將 nextTick 按照 promise 方式去書寫(暫且用的較少)。
應用場景
場景一、點擊按鈕顯示原本以 v-show = false 隱藏起來的輸入框,并獲取焦點。
<input id="keywords" v-if="showit"> showInput(){ this.showit = true document.getElementById("keywords").focus() }
以上的寫法在第一個 tick 里,因為獲取不到輸入框,自然也獲取不到焦點。如果我們改成以下的寫法,在 DOM 更新后就可以獲取到輸入框焦點了。
showsou(){ this.showit = true this.$nextTick(function () { // DOM 更新了 document.getElementById("keywords").focus() }) }
場景二、獲取元素屬,點擊獲取元素寬度。
<div id="app"> <p ref="myWidth" v-if="showMe">{{ message }}</p> <button @click="getMyWidth">獲取p元素寬度</button> </div> getMyWidth() { this.showMe = true; thisthis.message = this.$refs.myWidth.offsetWidth; //報錯 TypeError: this.$refs.myWidth is undefined this.$nextTick(()=>{ //dom元素更新后執行,此時能拿到p元素的屬性 thisthis.message = this.$refs.myWidth.offsetWidth; }) }
看完上述內容,你們對如何從Javascript事件循環看出Vue.nextTick的原理和執行機制有進一步的了解嗎?如果還想了解更多知識或者相關內容,請關注億速云行業資訊頻道,感謝大家的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。