Vue nextTick延遲回調(diào)獲取更新后DOM機制詳解
簡述
相信大家在寫vue項目的時候,一定會發(fā)現(xiàn)一個神奇的api,Vue.nextTick
。為什么說它神奇呢,那是因為在你做某些操作不生效時,將操作寫在Vue.nextTick
內(nèi),就神奇的生效了。那這是什么原因呢?
讓我們一起來研究一下。
- vue 實現(xiàn)響應式并不是數(shù)據(jù)發(fā)生變化后 DOM 立即變化,而是按照一定策略異步執(zhí)行 DOM 更新的
- vue 在修改數(shù)據(jù)后,視圖不會立刻進行更新,而是要等同一事件循環(huán)機制內(nèi)所有數(shù)據(jù)變化完成后,再統(tǒng)一進行DOM更新
nextTick
可以讓我們在下次 DOM 更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào),用于獲得更新后的 DOM。
事件循環(huán)機制
在討論Vue.nextTick
之前,需要先搞清楚事件循環(huán)機制,算是實現(xiàn)的基石了,那我們來看一下。
在瀏覽器環(huán)境中,我們可以將我們的執(zhí)行任務(wù)分為宏任務(wù)和微任務(wù),
- 宏任務(wù): 包括整體代碼script,
setTimeout
,setInterval
、setImmediate
、 I/O 操作、UI 渲染 - 微任務(wù):
Promise.then
、MuationObserver
事件循環(huán)的順序,決定js代碼的執(zhí)行順序。事件循環(huán)如下:
用代碼解釋,瀏覽器中事件循環(huán)的順序同如下代碼:
for (macroTask of macroTaskQueue) { // 1. 執(zhí)行一個宏任務(wù) handleMacroTask(); // 2. 執(zhí)行所有的微任務(wù) for (microTask of microTaskQueue) { handleMicroTask(microTask); } }
vue數(shù)據(jù)驅(qū)動視圖的處理(異步變化DOM)
<template> <div> <div>{{count}}</div> <div @click="handleClick">click</div> </div> </template> export default { data () { return { number: 0 }; }, methods: { handleClick () { for(let i = 0; i < 10000; i++) { this.count++; } } } }
分析上述代碼:
- 當點擊按鈕時,count會被循環(huán)改變10000次。那么每次count+1,都會觸發(fā)count的
setter
方法,然后修改真實DOM。按此邏輯,這整個過程,DOM會被更新10000次,我們都知道DOM的操作是非常昂貴的,而且這樣的操作完全沒有必要。所以vue內(nèi)部在派發(fā)更新時做了優(yōu)化 - 也就是,并不會每次數(shù)據(jù)改變都觸發(fā) watcher 的回調(diào),而是把這些 watcher 先添加到一個隊列queueWatcher里,然后在 nextTick 后執(zhí)行 flushSchedulerQueue處理
- 當 count 增加 10000 次時,vue內(nèi)部會先將對應的 Watcher 對象給 push 進一個隊列 queue 中去,等下一個 tick 的時候再去執(zhí)行。并不需要在下一個 tick 的時候執(zhí)行 10000 個同樣的 Watcher 對象去修改界面,而是只需要執(zhí)行一個 Watcher 對象,使其將界面上的 0 變成 10000 即可
Vue.nextTick原理
由上一節(jié)我們知道,Vue中 數(shù)據(jù)變化 => DOM變化 是異步過程,一旦觀察到數(shù)據(jù)變化,Vue就會開啟一個任務(wù)隊列,然后把在同一個事件循環(huán) (Event loop) 中觀察到數(shù)據(jù)變化的 Watcher
(Vue源碼中的Wacher類是用來更新Dep類收集到的依賴的)推送進這個隊列。
如果這個watcher被觸發(fā)多次,只會被推送到隊列一次。這種緩沖行為可以有效的去掉重復數(shù)據(jù)造成的不必要的計算和DOM操作。而在下一個事件循環(huán)時,Vue會清空隊列,并進行必要的DOM更新。
nextTick
的作用是為了在數(shù)據(jù)變化之后等待 Vue 完成更新 DOM ,可以在數(shù)據(jù)變化之后立即使用 Vue.nextTick(callback)
,JS是單線程的,擁有事件循環(huán)機制,nextTick
的實現(xiàn)就是利用了事件循環(huán)的宏任務(wù)和微任務(wù)。
vue中next-tick.js的源碼如下
import { noop } from 'shared/util' import { handleError } from './error' import { isIE, isIOS, isNative } from './env' export let isUsingMicroTask = false // 首先定義一個 callbacks 數(shù)組用來存儲 nextTick,在下一個 tick 處理這些回調(diào)函數(shù)之前, // 所有的 cb 都會被存在這個 callbacks 數(shù)組中 const callbacks = [] // pending 是一個標記位,代表一個等待的狀態(tài) let pending = false // 最后執(zhí)行 flushCallbacks() 方法,遍歷callbacks數(shù)組,依次執(zhí)行里邊的每個函數(shù) function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } let timerFunc /* 判斷采用哪種異步回調(diào)方式 由于微任務(wù)優(yōu)先級高,首先嘗試微任務(wù)模擬 1.首先嘗試使用Promise.then(微任務(wù)) 2.嘗試使用MuationObserver(微任務(wù))回調(diào) 3.嘗試使用 setImmediate(宏任務(wù))回調(diào) 4.最后嘗試使用setTimeout(宏任務(wù))回調(diào) */ if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) if (isIOS) setTimeout(noop) } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(flushCallbacks) } } else { timerFunc = () => { setTimeout(flushCallbacks, 0) } } export function nextTick (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() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
目前瀏覽器平臺并沒有實現(xiàn) nextTick 方法,所以 Vue.js 源碼中分別用 Promise、setTimeout、setImmediate 等方式在 microtask(或是task)中創(chuàng)建一個事件,目的是在當前調(diào)用棧執(zhí)行完畢以后(不一定立即)才會去執(zhí)行這個事件。
nextTick的調(diào)用方式
回調(diào)函數(shù)方式:Vue.nextTick(callback)
Promise方式:Vue.nextTick().then(callback)
實例方式:vm.$nextTick(callback)
Vue.nextTick的應用
created生命周期中操作DOM
created鉤子函數(shù)執(zhí)行的時候DOM 其實并未進行掛載和渲染,此時就是無法操作DOM的,我們將操作DOM的代碼中放到nextTick中,等待下一輪事件循環(huán)開始,DOM就已經(jīng)進行掛載好了,而與這個操作對應的就是mounted鉤子函數(shù),因為在mounted執(zhí)行的時候所有的DOM掛載已完成。
created(){ vm.$nextTick(() => { //不使用this.$nextTick()方法操作DOM會報錯 this.$refs.test.innerHTML="created中操作了DOM" }); }
修改數(shù)據(jù),獲取DOM值
當我們修改了data里的數(shù)據(jù)時,并不能立刻通過操作DOM去獲取到里面的值
<template> <div class="test"> <p ref='msg' id="msg">{{msg}}</p> </div> </template> <script> export default { name: 'Test', data () { return { msg:"hello world", } }, methods: { changeMsg() { this.msg = "hello Vue" // vue數(shù)據(jù)改變,改變了DOM里的innerText let msgEle = this.$refs.msg.innerText //后續(xù)js對dom的操作 console.log(msgEle) // hello world // 輸出可以看到data里的數(shù)據(jù)修改后DOM并沒有立即更新,后續(xù)的DOM不是最新的 this.$nextTick(() => { console.log(this.$refs.msg.innerText) // hello Vue }) this.$nextTick().then(() => { console.log(this.$refs.msg.innerText) // hello Vue }) }, changeMsg2() { this.$nextTick(() => { console.log(this.$refs.msg.innerText) // 1.hello world }) this.msg = "hello Vue" // 2. console.log(this.$refs.msg.innerText) // hello world this.$nextTick().then(() => { console.log(this.$refs.msg.innerText) // hello Vue }) // nextTick中先添加的先執(zhí)行,執(zhí)行1后,才會執(zhí)行2(Vue操作Dom的異步) } } } </script>
v-show/v-if由隱藏變?yōu)轱@示
點擊按鈕顯示原本以 v-show=false或v-if 隱藏起來的輸入框,并獲取焦點或者獲得寬高等的場景
以上就是Vue nextTick延遲回調(diào)獲取更新后DOM機制詳解的詳細內(nèi)容,更多關(guān)于Vue nextTick延遲回調(diào)DOM獲取的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue數(shù)據(jù)更新視圖不更新的幾種解決方案小結(jié)
這篇文章主要介紹了Vue數(shù)據(jù)更新視圖不更新的幾種解決方案小結(jié),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08