工做中,用vue開發,常常會碰到用數據驅動dom,而後操做dom卻沒有效果的狀況。若是有用到tab切換加上echarts展現,確定是氣的想砸桌子。下面來談談vue中dom的刷新。javascript
所謂異步更新,就是vue中用數據去驅動dom,數據變化了,DOM卻不會當即的更新,而是在下一個Tick中更新dom。固然,vue中手動操做DOM,DOM是當即刷新的。 什麼是數據驅動DOM?其實就是聲名一個響應式的數據,當數據改變時,不用手動的操做dom,vue會自動的更新視圖。 先來簡單的看下代碼 vue
在瀏覽器中看vue的dom異步刷新 java
. 從圖中能夠看出,當showDom發生變化的時候,‘有夢想的api搬運工’ 本應該隱藏的,而卻沒有隱藏,他會等到下一個隊列中去刷新,這就是所謂的dom異步刷新。一樣的,若是工做中經過數據去驅動dom,而後當即的去操做這個dom,有很大機率很報錯哦。 看看手動的操做dom是什麼結果呢。 從上圖能夠看出,當手動操做dom時候,當修改dom後,顏色是馬上變化的。這樣就不會碰到使用異步dom出現的問題了。若是在vue中使用了數據去驅動dom,碰到了問題該怎麼辦呢?固然是今天的主角$nextTick去解決了。什麼是nextTick?套用官網的話來講 將回調延遲到下次 DOM 更新循環以後執行。在修改數據以後當即使用它,而後等待 DOM 更新。它跟全局方法 Vue.nextTick 同樣,不一樣的是回調的 this 自動綁定到調用它的實例上。。 通俗的說,當nextTick的裏的方法會等到當前dom更新完之後觸發。 面試
.那下面一段代碼,固然就是很輕易的解釋了。 算法
. 界面初始化進來,hellow 的初始化值就是 'hello', 改變他hellow的值,而後當即去獲取,因爲異步更新,dom未發生變化。在nextTick中獲取到了真正的值。vue的異步刷新,用到了promise,MutationObserver,和setTimeout這三個api,真正理解這個三個api,是須要熟悉瀏覽器的,若是對瀏覽器eventloop 和microtask,macrotask還不熟悉的,請左拐,等會兒再來這兒看。從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理. 別忘記回來哦,後面的更精彩。下面的vue源碼須要用到此部分的知識。api
談到vue的異步更新,不得不談到vdom(虛擬dom)。什麼是vdom?虛擬dom就是一個javascript對象,這個對象內部有許多dom的屬性,以此來模擬一個真正的dom對象。而vue中所操做的dom,就是這個vdom,等到全部dom完成,把vdom掛載到真實的dom下,減小對真實dom的操做。這裏就不展開多講了。數組
到底異步刷新有什麼優勢呢?看下面兩端代碼。 promise
.若是經過vue提供的數據驅動的方式異步刷新dom,add()方法,numbershu改變後,dom並不會當即的刷新,等到for循環結束後,獲得最終值後,更新一次dom。dom的重繪與迴流只發生一次。 而經過computedNum()方法,每一次的for循環,都會觸發一次視圖的更新,引起屢次的dom的重繪與迴流,這種代碼質量不敢恭維哪!! 而若是把computedNum()方法改爲這樣瀏覽器
computedNum() {
let m = 0;
for(let i = 0; i < 100 ;i ++) {
m = i;
}
this.$refs.computedNum = m;
}
複製代碼
若是改寫成這樣,dom只會刷新一次。反而比vue使用vdom進行diff算法進行計算,而後更新dom,性能更加的好。 關於vue的vdom設計理念,其實就是在用戶在不考慮性能優化的狀況下,替用戶進行看得過去的優化,並不能保證vue中的dom操做都是最優選擇,只是讓用戶開發的更爽。性能優化
說到vue異步更新dom得策略,得先看一下nextTick的實現原理。
let callbacks = [];
let pending = false;
let timerFunc
if(typeof Promise !== 'undefined) { //判斷當前瀏覽器是否支持promise,如支持,用promise實現異步刷新dom const p = Promise.resolve(); timerFunc = () => { p.then(flushCallbacks) } } else if(typeof MutationObserver !== 'undefined') { let counter = 1; const observe = new MutationObserver(flushCallbacks); const textNode = document.createTextNode(String(counter)); observe.observe(textNode,{ characterData: true }) timerFunc = () => { conter = (conter + 1) % 2; textNode.data = String(counter) } } else { timerFunc = () => { setTimeout(flushCallbacks,0) } } function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } function nextTick(cb,ctx) { //ctx是vue實例 let _resolve; callbacks.push(() => { if(cb) { cb.call(ctx) //官網對nextTick的解釋 自動綁定調用他的實例,就是vue的實例 } else if (_resolve) { _resolve(ctx); } }) if(!pending) { pending = true; timerFunc(); } if(!cb && type Promise !== 'undefined) {
return new Promise(resolve => {
_resolve = resolve
})
}
}
複製代碼
這裏把實現nextTick最重要的三部分扣了出來,而且簡化了一下,若是有感興趣的同窗,能夠去源碼裏面看完整版的。 1.首先 nextTick須要傳入一個回調函數,在當前執行棧內,當第一次調用nextTick方法的時候,callbacks裏push回調函數,此時,pending的值是默認的false,而後改變pending,執行timerFun();p.then異步執行flushCallbacks函數,其實就是執行callbacks數組裏的函數。在當前執行棧內,若是有第二次調用nextTick函數,繼續向callbacks裏推入回調函數。可是pending已是true了,不會在重複調用timerFunc,因爲flushCallbacks是異步執行的,callbacks內的回調還未執行,又向callbacks推入一個新的回調函數,此時callbacks數組裏有兩個回調,以此類推。等到當前棧任務執行完,開始執行flushCallbacks,改變pending的狀態,爲下一個隊列作鋪墊。此時callbacks數組內已經有n個回調,而後執行這些函數。 2. 到底採用哪一個方法去異步執行?根據上面的判斷,若是有promise,就使用promise,若是沒有,就使用MutationObserver,若是尚未,就使用setTimeout。(IE:大家都看我幹啥?我長得漂亮?)。簡單說一下MutationObserve,h5新出來的api,當dom變化時,會觸發回調。和promise同樣,都是microtask任務。點我看MutationObserver的相關api。 3.根據源碼我知道了,nextTick還能看成一個promise使用。this.nexTick()不能傳函數喲。
談到vue,確定張口就來,經過defineProperty重寫get與set方法,實現數據的雙向綁定。畢竟面試必備的一句話。 下面來談一談dom是如何異步更新的。 當vue中的的響應式數據發生變化,經過set方法,會調用Watch類的update()方法,而update方法會調用queueWatcher方法來更新視圖,看一下queueWatcher的定義
let waiting = false;
let index = 0;
let has = {};
...
...
export function queueWatcher (watcher) { // watch的實例
const id = watcher.id //每一個響應式數據中都有一個獨立watch.id
if (has[id] == null) { // 若是一個響應式數據屢次改變,只會向queue數組中推入一次,視圖刷新只更新最終的值。(還記得上面的add()方法嗎,numbershu這個變量只更新一次)
has[id] = true
queue.push(watcher)
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) { // 若是是設置的不是異步更新,就執行更新dom。(好像歷來沒有用到過)
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
function flushSchedulerQueue () {
flushing = true
let watcher, id
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
}
複製代碼
上面的代碼也都是簡化的代碼,便於理解,若是對原版感興趣的,能夠去看vue源碼。
1.當第一次響應式數據變化後,queueWatcher方法會向queue隊列中添加一個更新視圖的回調函數,若是queue中已經有了這個實例,就不添加。而flushSchedulerQueue這個方法就是遍歷執行queue這個數組內的函數,queueWatcher這個方法最後調用了nextTick(flushSchedulerQueue),上面講到過,nextTick會把回調放入到callbacks的數組內,這裏是異步執行。因此calbacks裏會有一個flushSchedulerQueue的函數。而flushSchedulerQueue內又有一個queue隊列,當前執行棧內,若是有第二個響應式數據發生變化,又會向還未遍歷的queue隊列中添加watch實例,以此類推。當前執行棧任務結束後,調用任務隊列中的callbacks內的回調函數,調用到flushSchedulerQueue函數時,此函數又會遍歷調用queue的回調函數,最終調用watch.run()方法去更新視圖。
說白了,vue的異步更新,最終的calbacks數組結構就相似[fn,fn,flushSchedulerQueue,fn]。
flushSchedulerQueue函數內部又有一個queue數組等待去遍歷,其結構相似[fn,fn,fn],固然純屬猜想。
❤️ 若是各位看官看的還進行,請給一個贊,順手點個關注,就是對個人最大支持