您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“Vue異步更新機制及$nextTick原理是什么”,內容詳細,步驟清晰,細節處理妥當,希望這篇“Vue異步更新機制及$nextTick原理是什么”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
可能你還沒有注意到,Vue異步執行DOM更新。只要觀察到數據變化,Vue將開啟一個隊列,并緩沖在同一事件循環中發生的所有數據改變。如果同一個watcher被多次觸發,只會被推入到隊列中一次。這種在緩沖時去除重復數據對于避免不必要的計算和DOM操作上非常重要。然后,在下一個的事件循環“tick”中,Vue刷新隊列并執行實際 (已去重的) 工作。
當我們在更新數據后立馬去獲取DOM中的內容是會發現獲取的依然還是舊的內容。
<template> <div class="next_tick"> <div ref="title" class="title">{{name}}</div> </div> </template> <script> export default { data() { return { name: '前端南玖' } }, mounted() { this.name = 'front end' console.log('sync',this.$refs.title.innerText) this.$nextTick(() => { console.log('nextTick',this.$refs.title.innerText) }) } } </script>
從圖中我們可以發現數據改變后同步獲取dom元素中的內容是老的數據,而在nextTick里面獲取的是更新后的數據,這是為什么呢?
其實這里你用微任務或宏任務去獲取dom元素中的內容也是更新后的數據,我們可以來試試:
mounted() { this.name = 'front end' console.log('sync',this.$refs.title.innerText) Promise.resolve().then(() => { console.log('微任務',this.$refs.title.innerText) }) setTimeout(() => { console.log('宏任務',this.$refs.title.innerText) }, 0) this.$nextTick(() => { console.log('nextTick',this.$refs.title.innerText) }) }
是不是覺得有點不可思議,其實沒什么奇怪的,在vue源碼中它的實現原理就是利用的微任務與宏任務,慢慢往下看,后面會一一解釋。
沒錯,vue中的DOM更新還是批量處理的,這樣做的好處無疑就是能夠最大程度的優化性能。OK這里也有看點,別著急
vue同時更新了多個數據,你覺得dom是更新多次還是更新一次?我們來試試
<template> <div class="next_tick"> <div ref="title" class="title">{{name}}</div> <div class="verse">{{verse}}</div> </div> </template> <script> export default { name: 'nextTick', data() { return { name: '前端南玖', verse: '如若東山能再起,大鵬展翅上九霄', count:0 } }, mounted() { this.name = 'front end' this.verse = '世間萬物都是空,功名利祿似如風' // console.log('sync',this.$refs.title.innerText) // Promise.resolve().then(() => { // console.log('微任務',this.$refs.title.innerText) // }) // setTimeout(() => { // console.log('宏任務',this.$refs.title.innerText) // }, 0) // this.$nextTick(() => { // console.log('nextTick',this.$refs.title.innerText) // }) }, updated() { this.count++ console.log('update:',this.count) } } </script> <style lang="less"> .verse{ font-size: (20/@rem); } </style>
我們可以看到updated鉤子只執行了一次,說明我們同時更新了多個數據,DOM只會更新一次
再來看另一種情況,同步與異步混合,DOM會更新幾次?
mounted() { this.name = 'front end' this.verse = '世間萬物都是空,功名利祿似如風' Promise.resolve().then(() => { this.name = 'study ...' }) setTimeout(() => { this.verse = '半身風雨半身寒,一杯濁酒敬流年' }) // console.log('sync',this.$refs.title.innerText) // Promise.resolve().then(() => { // console.log('微任務',this.$refs.title.innerText) // }) // setTimeout(() => { // console.log('宏任務',this.$refs.title.innerText) // }, 0) // this.$nextTick(() => { // console.log('nextTick',this.$refs.title.innerText) // }) }, updated() { this.count++ console.log('update:',this.count) }
從圖中我們會發現,DOM會渲染三次,分別是同步的一次(2個同步一起更新),微任務的一次,宏任務的一次。并且在用setTimeout更新數據時會明顯看見頁面數據變化的過程。(這句話是重點,記好小本本)這也就是為什么nextTick源碼中setTimeout做最后兜底用的,優先使用微任務。
沒錯,這里跟事件循環還有很大的關系,這里稍微提一下,更詳細可以看探索JavaScript執行機制
由于JavaScript是單線程的,這就決定了它的任務不可能只有同步任務,那些耗時很長的任務如果也按同步任務執行的話將會導致頁面阻塞,所以JavaScript任務一般分為兩類:同步任務與異步任務,而異步任務又分為宏任務與微任務。
宏任務: script(整體代碼)、setTimeout、setInterval、setImmediate、I/O、UI rendering
微任務: promise.then、MutationObserver
同步任務直接放入到主線程執行,異步任務(點擊事件,定時器,ajax等)掛在后臺執行,等待I/O事件完成或行為事件被觸發。
系統后臺執行異步任務,如果某個異步任務事件(或者行為事件被觸發),則將該任務添加到任務隊列,并且每個任務會對應一個回調函數進行處理。
這里異步任務分為宏任務與微任務,宏任務進入到宏任務隊列,微任務進入到微任務隊列。
執行任務隊列中的任務具體是在執行棧中完成的,當主線程中的任務全部執行完畢后,去讀取微任務隊列,如果有微任務就會全部執行,然后再去讀取宏任務隊列
上述過程會不斷的重復進行,也就是我們常說的 「事件循環(Event-Loop)」。
總的來說,在事件循環中,微任務會先于宏任務執行。而在微任務執行完后會進入瀏覽器更新渲染階段,所以在更新渲染前使用微任務會比宏任務快一些,一次循環就是一次tick 。
在一次event loop中,microtask在這一次循環中是一直取一直取,直到清空microtask隊列,而macrotask則是一次循環取一次。
如果執行事件循環的過程中又加入了異步任務,如果是macrotask,則放到macrotask末尾,等待下一輪循環再執行。如果是microtask,則放到本次event loop中的microtask任務末尾繼續執行。直到microtask隊列清空。
在Vue中DOM更新一定是由于數據變化引起的,所以我們可以快速找到更新DOM的入口,也就是set時通過dep.notify
通知watcher更新的時候
// watcher.js // 當依賴發生變化時,觸發更新 update() { if(this.lazy) { // 懶執行會走這里, 比如computed this.dirty = true }else if(this.sync) { // 同步執行會走這里,比如this.$watch() 或watch選項,傳遞一個sync配置{sync: true} this.run() }else { // 將當前watcher放入watcher隊列, 一般都是走這里 queueWatcher(this) } }
從這里我們可以發現vue默認就是走的異步更新機制,它會實現一個隊列進行緩存當前需要更新的watcher
// scheduler.js /*將一個觀察者對象push進觀察者隊列,在隊列中已經存在相同的id則該觀察者對象將被跳過,除非它是在隊列被刷新時推送*/ export function queueWatcher (watcher: Watcher) { /*獲取watcher的id*/ const id = watcher.id /*檢驗id是否存在,已經存在則直接跳過,不存在則標記在has中,用于下次檢驗*/ if (has[id] == null) { has[id] = true // 如果flushing為false, 表示當前watcher隊列沒有在被刷新,則watcher直接進入隊列 if (!flushing) { queue.push(watcher) } else { // 如果watcher隊列已經在被刷新了,這時候想要插入新的watcher就需要特殊處理 // 保證新入隊的watcher刷新仍然是有序的 let i = queue.length - 1 while (i >= 0 && queue[i].id > watcher.id) { i-- } queue.splice(Math.max(i, index) + 1, 0, watcher) } // queue the flush if (!waiting) { // wating為false,表示當前瀏覽器的異步任務隊列中沒有flushSchedulerQueue函數 waiting = true // 這就是我們常見的this.$nextTick nextTick(flushSchedulerQueue) } } }
ok,從這里我們就能發現vue并不是跟隨數據變化立即更新視圖的,它而是維護了一個watcher隊列,并且id重復的watcher只會推進隊列一次,因為我們關心的只是最終的數據,而不是它更新多少次。等到下一個tick時,這些watcher才會從隊列中取出,更新視圖。
nextTick的目的就是產生一個回調函數加入task或者microtask中,當前棧執行完以后(可能中間還有別的排在前面的函數)調用該回調函數,起到了異步觸發(即下一個tick時觸發)的目的。
// next-tick.js const callbacks = [] let pending = false // 批處理 function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 // 依次執行nextTick的方法 for (let i = 0; i < copies.length; i++) { copies[i]() } } export function nextTick (cb, ctx) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) // 因為內部會調nextTick,用戶也會調nextTick,但異步只需要一次 if (!pending) { pending = true timerFunc() } // 執行完會會返回一個promise實例,這也是為什么$nextTick可以調用then方法的原因 if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
兼容性處理,優先使用promise.then 優雅降級(兼容處理就是一個不斷嘗試的過程,誰可以就用誰。
Vue 在內部對異步隊列嘗試使用原生的 Promise.then、MutationObserver 和 setImmediate,如果執行環境不支持,則會采用 setTimeout(fn, 0) 代替。
// timerFunc // promise.then -> MutationObserver -> setImmediate -> setTimeout // vue3 中不再做兼容性處理,直接使用的就是promise.then 任性 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) // 可以監聽DOM變化,監聽完是異步更新的 // 但這里并不是想用它做DOM監聽,而是利用它是微任務這一特點 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 { // Fallback to setTimeout. timerFunc = () => { setTimeout(flushCallbacks, 0) } }
我們平常調用的$nextTick
其實就是上面這個方法,只不過在源碼中renderMixin
中將該方法掛在了vue的原型上方便我們使用
export function renderMixin (Vue) { // install runtime convenience helpers installRenderHelpers(Vue.prototype) Vue.prototype.$nextTick = function (fn) { return nextTick(fn, this) } Vue.prototype._render = function() { //... } // ... }
上面說了那么多,相信大家對Vue的異步更新機制以及$nextTick
原理已經有了初步的了解。每一輪事件循環的最后會進行一次頁面渲染,并且從上面我們知道渲染過程也是個宏任務,這里可能會有個誤區,那就是DOM tree的修改是同步的,只有渲染過程是異步的,也就是說我們在修改完DOM后能夠立即獲取到更新的DOM,不信我們可以來試一下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="title">欲試人間煙火,怎料世道滄桑</div> <script> title.innerText = '萬卷詩書無一用,半老雄心剩疏狂' console.log('updated',title) </script> </body> </html>
答案很明顯,因為Vue處于性能考慮,Vue會將用戶同步修改的多次數據緩存起來,等同步代碼執行完,說明這一次的數據修改就結束了,然后才會去更新對應DOM,一方面可以省去不必要的DOM操作,比如同時修改一個數據多次,只需要關心最后一次就好了,另一方面可以將DOM操作聚集,提升render性能。
看下面這個圖理解起來應該更容易一點
這個應該不用多說吧,因為微任務一定比宏任務優先執行,如果nextTick是微任務,它會在當前同步任務執行完立即執行所有的微任務,也就是修改DOM的操作也會在當前tick內執行,等本輪tick任務全部執行完成,才是開始執行UI rendering。如果nextTick是宏任務,它會被推進宏任務隊列,并且在本輪tick執行完之后的某一輪執行,注意,它并不一定是下一輪,因為你不確定宏任務隊列中它之前還有所少個宏任務在等待著。所以為了能夠盡快更新DOM,Vue中優先采用的是微任務,并且在Vue3中,它沒有了兼容判斷,直接使用的是promise.then
微任務,不再考慮宏任務了。
讀到這里,這篇“Vue異步更新機制及$nextTick原理是什么”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。