Vue之關(guān)于異步更新細節(jié)
前言
Vue官網(wǎng)對于異步更新的介紹如下:
- Vue 在更新 DOM 時是異步執(zhí)行的。
- 只要偵聽到數(shù)據(jù)變化,Vue 將開啟一個隊列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更。
- 如果同一個 watcher 被多次觸發(fā),只會被推入到隊列中一次。
- 這種在緩沖時去除重復(fù)數(shù)據(jù)對于避免不必要的計算和 DOM 操作是非常重要的
Vue使用Object.defineProperty對數(shù)據(jù)劫持后,當對對象進行set操作,就會觸發(fā)視圖更新。
更新邏輯
以下面實例來分析視圖更新處理邏輯:
<div>{{ message }}</div> <button @click="handleClick">更新</button> new Vue({ data: { message: '' }, methods: { handleClick() { this.message = Date.now(); } } })
當點擊更新按鈕后會對已劫持的屬性message做賦值操作,此時會觸發(fā)Object.defineProperty的set操作。
Object.defineProperty set操作
Object.defineProperty的set函數(shù)的設(shè)置,實際上最核心的邏輯就是觸發(fā)視圖更新,具體代碼邏輯如下:
set: function reactiveSetter (newVal) { // 其他邏輯 // 觸發(fā)視圖更新 dep.notify(); }
每個屬性都會對應(yīng)一個Dep對象,當對屬性進行賦值時就會調(diào)用Dep的notify實例方法,該實例方法的功能就是是通知視圖需要更新。
Dep notify實例方法
notify實例方法的代碼邏輯如下:
Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice(); for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } };
subs中存儲是watcher對象,每個Vue實例都存在一個與視圖更新關(guān)聯(lián)的watcher對象,該對象的創(chuàng)建是在$mount階段,具體看查看之前的文章Vue實例創(chuàng)建整體流程。
代表屬性的Dep對象與watcher對象的關(guān)聯(lián)是在render函數(shù)調(diào)用階段具體屬性獲取時建立的即依賴收集
notify方法會執(zhí)行與當前屬性關(guān)聯(lián)的所有watcher對象的update方法,必然會存在一個視圖更新相關(guān)的watcher。
watcher對象的按照分類實際上分為兩類:
- 視圖更新相關(guān)的,每一個Vue實例都存在一個此類的watcher對象
- 邏輯計算相關(guān)的,計算屬性和watch監(jiān)聽所創(chuàng)建的watcher對象
Watcher update實例方法
update實例方法的代碼邏輯具體如下:
Watcher.prototype.update = function update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); } };
lazy、sync都是Watcher的屬性,分別表示:
- lazy:表示懶處理,即延遲相關(guān)處理,用于處理計算屬性
- computedsync:表示同步執(zhí)行,即觸發(fā)屬性更新就立即更新視圖
從上面邏輯中可知,默認是queueWatcher處理即開啟一個隊列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更,即視圖是異步更新的。
這里需要注意的一點是:
queueWatcher中必然存在視圖更新的watcher對象,不會存在計算屬性computed對應(yīng)的watcher(computed對應(yīng)的watcher對象lazy屬性默認為true),可能存在watch API對應(yīng)的用戶性質(zhì)的watcher對象
queueWatcher執(zhí)行邏輯
function queueWatcher (watcher) { var id = watcher.id; if (has[id] == null) { has[id] = true; if (!flushing) { queue.push(watcher); } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. var i = queue.length - 1; while (i > index && queue[i].id > watcher.id) { i--; } queue.splice(i + 1, 0, watcher); } // queue the flush if (!waiting) { waiting = true; nextTick(flushSchedulerQueue); } } }
實際上面邏輯主要分成3點:
- 對于同一個watcher對象,使用has對象結(jié)構(gòu)+id為key來判斷隊列中是否已存在對應(yīng)watcher對象,如果存在就不會將其添加到queue中
- 通過flushing標識區(qū)分當在清空隊列過程中和正常情況下,如何向queue中添加watcher
- 通過waiting標識區(qū)分是否要執(zhí)行nextTick即清空queue的動作
因為queue是全局變量,在此步驟之前就將watcher對象添加到queue,如果waiting為true就標識已經(jīng)調(diào)用nextTick實現(xiàn)異步處理queue了,就不要再次調(diào)用nextTick
從上面整體邏輯可知,queueWacther的邏輯主要就兩點:
- 判斷是否重復(fù)watcher,對于不重復(fù)的watcher將其添加到queue中
- 調(diào)用nextTick開啟異步處理queue操作即flushSchedulerQueue函數(shù)執(zhí)行
nextTick + flushSchedulerQueue
nextTick函數(shù)實際上跟$nextTick是相同的邏輯,主要的區(qū)別就是上下文的不同,即函數(shù)的this綁定值的不同。
使用macroTask API還是microTask API來執(zhí)行flushSchedulerQueue
而flushSchedulerQueue函數(shù)就是queue的具體處理邏輯,主要邏輯如下:
function flushSchedulerQueue () { flushing = true; var watcher, id; // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child) // 2. A component's user watchers are run before its render watcher (because // user watchers are created before the render watcher) // 3. If a component is destroyed during a parent component's watcher run, // its watchers can be skipped. queue.sort(function (a, b) { return a.id - b.id; }); for (index = 0; index < queue.length; index++) { watcher = queue[index]; id = watcher.id; has[id] = null; watcher.run(); } var activatedQueue = activatedChildren.slice(); var updatedQueue = queue.slice(); resetSchedulerState(); // call component updated and activated hooks callActivatedHooks(activatedQueue); callUpdatedHooks(updatedQueue); }
flushSchedulerQueue函數(shù)的主要邏輯可以總結(jié)成如下幾點:
- 對隊列queue中watcher對象進行排序
- 遍歷queue執(zhí)行每個watcher對象的run方法
- 重置控制queue的相關(guān)狀態(tài),用于下一輪更新
- 執(zhí)行組件的updated和activated生命周期
這里就不展開了,需要注意的是activated是針對于keep-alive下組件的特殊處理,updated生命周期是先子組件再父組件的,隊列queue的watcher對象是按照父組件子組件順序排列的,所以在源碼中updated生命周期的觸發(fā)是倒序遍歷queue觸發(fā)的。
首先說說watcher對象的run實例方法,該方法的主要邏輯就是執(zhí)行watcher對象的getter屬性和cb屬性對應(yīng)的函數(shù)。
上面說過watcher對象的按照分類實際上分為兩類:
- 視圖更新相關(guān)的,每一個Vue實例都存在一個此類的watcher對象
- 邏輯計算相關(guān)的,計算屬性和watch監(jiān)聽所創(chuàng)建的watcher對象
watcher對象的getter屬性和cb屬性就是對應(yīng)著上面各類watcher的實際處理邏輯,例如watch API對應(yīng)的getter屬性就是監(jiān)聽項,cb屬性才是具體的處理邏輯。
為什么需要對queue中watcher對象進行排序?
實際上Vue源碼中有相關(guān)說明,這主要涉及到嵌套組件Vue實例創(chuàng)建、render watch和用戶watch創(chuàng)建的時機。
每個組件都是一個Vue實例,嵌套組件創(chuàng)建總是從父組件Vue實例開始創(chuàng)建的,在父組件patch階段才創(chuàng)建子組件的Vue實例。
而這個順序決定了watcher對象的id值大小問題:
父組件的所有watcher對象id < 子組件的所有watcher對象id
render watch實際上就是與視圖更新相關(guān)的watcher對象,該對象是其對應(yīng)的Vue實例創(chuàng)建的末期即掛載階段才創(chuàng)建的,是晚于用戶watch即計算屬性computed和watch API創(chuàng)建的watcher對象,所以:
render watch的id < 所有用戶watch的id的
子組件可能是更新觸發(fā)源,如果父組件也需要更新視圖,這樣queue隊列中子組件的watcher對象位置會在父組件的watcher對象之前,對queue中watcher對象進行排序就保證了:
視圖更新時 父組件 總是先于 子組件開始更新操作,而每個組件對應(yīng)的視圖渲染的watcher最后再執(zhí)行(即用戶watcher對象對應(yīng)的邏輯先執(zhí)行)
總結(jié)
Vue異步更新的過程還是非常清晰的:
- 對屬性賦值觸發(fā)Dep對象notify方法執(zhí)行
- 繼而執(zhí)行Watcher對象的update方法將對象保存到隊列queue中
- 繼而調(diào)用mircoTask API或macroTask API執(zhí)行queue中任務(wù)
- 對隊列中watcher進行排序,保證順序執(zhí)行的正確性,調(diào)用其對應(yīng)run方法來實現(xiàn)視圖更新和相關(guān)邏輯更新操作
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
VueJs使用Amaze ui調(diào)整列表和內(nèi)容頁面
這篇文章主要介紹了VueJs 填坑日記之使用Amaze ui調(diào)整列表和內(nèi)容頁面,需要的朋友可以參考下2017-11-11Vue3路由配置createRouter、createWebHistory、useRouter和useRoute詳解
Vue3和Vue2基本差不多,只不過需要將createRouter、createWebHistory從vue-router中引入,再進行使用,下面這篇文章主要給大家介紹了關(guān)于Vue3路由配置createRouter、createWebHistory、useRouter和useRoute的相關(guān)資料,需要的朋友可以參考下2023-02-02vue cli3 實現(xiàn)分環(huán)境打包的步驟
這篇文章主要介紹了vue cli3 實現(xiàn)分環(huán)境打包的步驟,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03vue開發(fā)中數(shù)據(jù)更新但視圖不刷新的解決方法
在開發(fā)中我們處理數(shù)據(jù)時會遇到數(shù)據(jù)更新了,但視圖并沒有更新,這種情況往往是數(shù)據(jù)嵌套層數(shù)過多導(dǎo)致的問題,下面這篇文章主要給大家介紹了關(guān)于vue開發(fā)中數(shù)據(jù)更新但視圖不刷新的解決方法,需要的朋友可以參考下2022-11-11