批量異步更新策略及 nextTick 原理
為什么要異步更新
通過前面幾個(gè)章節(jié)我們介紹蜂莉,相信大家已經(jīng)明白了 Vue.js 是如何在我們修改 data
中的數(shù)據(jù)后修改視圖了送浊。簡單回顧一下仆救,這里面其實(shí)就是一個(gè)“setter -> Dep -> Watcher -> patch -> 視圖
”的過程。
假設(shè)我們有如下這么一種情況速和。
<template>
<div>
<div>{{number}}</div>
<div @click="handleClick">click</div>
</div>
</template>
export default {
data () {
return {
number: 0
};
},
methods: {
handleClick () {
for(let i = 0; i < 1000; i++) {
this.number++;
}
}
}
}
當(dāng)我們按下 click 按鈕的時(shí)候驶俊,number
會(huì)被循環(huán)增加1000次。
那么按照之前的理解嬉探,每次 number
被 +1 的時(shí)候擦耀,都會(huì)觸發(fā) number
的 setter
方法,從而根據(jù)上面的流程一直跑下來最后修改真實(shí) DOM涩堤。那么在這個(gè)過程中眷蜓,DOM 會(huì)被更新 1000 次!太可怕了胎围。
Vue.js 肯定不會(huì)以如此低效的方法來處理吁系。Vue.js在默認(rèn)情況下,每次觸發(fā)某個(gè)數(shù)據(jù)的 setter
方法后白魂,對(duì)應(yīng)的 Watcher
對(duì)象其實(shí)會(huì)被 push
進(jìn)一個(gè)隊(duì)列 queue
中汽纤,在下一個(gè) tick 的時(shí)候?qū)⑦@個(gè)隊(duì)列 queue
全部拿出來 run
( Watcher
對(duì)象的一個(gè)方法,用來觸發(fā) patch
操作) 一遍福荸。
那么什么是下一個(gè) tick 呢蕴坪?
nextTick
Vue.js 實(shí)現(xiàn)了一個(gè) nextTick
函數(shù),傳入一個(gè) cb
逞姿,這個(gè) cb
會(huì)被存儲(chǔ)到一個(gè)隊(duì)列中辞嗡,在下一個(gè) tick 時(shí)觸發(fā)隊(duì)列中的所有 cb
事件。
因?yàn)槟壳盀g覽器平臺(tái)并沒有實(shí)現(xiàn) nextTick
方法滞造,所以 Vue.js 源碼中分別用 Promise
续室、setTimeout
、setImmediate
等方式在 microtask(或是task)中創(chuàng)建一個(gè)事件谒养,目的是在當(dāng)前調(diào)用棧執(zhí)行完畢以后(不一定立即)才會(huì)去執(zhí)行這個(gè)事件挺狰。
筆者用 setTimeout
來模擬這個(gè)方法,當(dāng)然买窟,真實(shí)的源碼中會(huì)更加復(fù)雜丰泊,筆者在小冊(cè)中只講原理,有興趣了解源碼中 nextTick
的具體實(shí)現(xiàn)的同學(xué)可以參考next-tick始绍。
首先定義一個(gè) callbacks
數(shù)組用來存儲(chǔ) nextTick
瞳购,在下一個(gè) tick 處理這些回調(diào)函數(shù)之前,所有的 cb
都會(huì)被存在這個(gè) callbacks
數(shù)組中亏推。pending
是一個(gè)標(biāo)記位学赛,代表一個(gè)等待的狀態(tài)年堆。
setTimeout
會(huì)在 task 中創(chuàng)建一個(gè)事件 flushCallbacks
,flushCallbacks
則會(huì)在執(zhí)行時(shí)將 callbacks
中的所有 cb
依次執(zhí)行盏浇。
let callbacks = [];
let pending = false;
function nextTick (cb) {
callbacks.push(cb);
if (!pending) {
pending = true;
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]();
}
}
再寫 Watcher
第一個(gè)例子中变丧,當(dāng)我們將 number
增加 1000 次時(shí),先將對(duì)應(yīng)的 Watcher
對(duì)象給 push
進(jìn)一個(gè)隊(duì)列 queue
中去绢掰,等下一個(gè) tick 的時(shí)候再去執(zhí)行痒蓬,這樣做是對(duì)的。但是有沒有發(fā)現(xiàn)滴劲,另一個(gè)問題出現(xiàn)了攻晒?
因?yàn)?number
執(zhí)行 ++ 操作以后對(duì)應(yīng)的 Watcher
對(duì)象都是同一個(gè),我們并不需要在下一個(gè) tick 的時(shí)候執(zhí)行 1000 個(gè)同樣的 Watcher
對(duì)象去修改界面哑芹,而是只需要執(zhí)行一個(gè) Watcher
對(duì)象缀去,使其將界面上的 0 變成 1000 即可干茉。
那么唠梨,我們就需要執(zhí)行一個(gè)過濾的操作棵譬,同一個(gè)的 Watcher
在同一個(gè) tick 的時(shí)候應(yīng)該只被執(zhí)行一次,也就是說隊(duì)列 queue
中不應(yīng)該出現(xiàn)重復(fù)的 Watcher
對(duì)象末购。
那么我們給 Watcher
對(duì)象起個(gè)名字吧~用 id
來標(biāo)記每一個(gè) Watcher
對(duì)象破喻,讓他們看起來“不太一樣”。
實(shí)現(xiàn) update
方法盟榴,在修改數(shù)據(jù)后由 Dep
來調(diào)用曹质, 而 run
方法才是真正的觸發(fā) patch
更新視圖的方法。
let uid = 0;
class Watcher {
constructor () {
this.id = ++uid;
}
update () {
console.log('watch' + this.id + ' update');
queueWatcher(this);
}
run () {
console.log('watch' + this.id + '視圖更新啦~');
}
}
queueWatcher
不知道大家注意到了沒有擎场?筆者已經(jīng)將 Watcher
的 update
中的實(shí)現(xiàn)改成了
queueWatcher(this);
將 Watcher
對(duì)象自身傳遞給 queueWatcher
方法羽德。
我們來實(shí)現(xiàn)一下 queueWatcher
方法。
let has = {};
let queue = [];
let waiting = false;
function queueWatcher(watcher) {
const id = watcher.id;
if (has[id] == null) {
has[id] = true;
queue.push(watcher);
if (!waiting) {
waiting = true;
nextTick(flushSchedulerQueue);
}
}
}
我們使用一個(gè)叫做 has
的 map迅办,里面存放 id -> true ( false ) 的形式宅静,用來判斷是否已經(jīng)存在相同的 Watcher
對(duì)象 (這樣比每次都去遍歷 queue
效率上會(huì)高很多)。
如果目前隊(duì)列 queue
中還沒有這個(gè) Watcher
對(duì)象站欺,則該對(duì)象會(huì)被 push
進(jìn)隊(duì)列 queue
中去姨夹。
waiting
是一個(gè)標(biāo)記位,標(biāo)記是否已經(jīng)向 nextTick
傳遞了 flushSchedulerQueue
方法矾策,在下一個(gè) tick 的時(shí)候執(zhí)行 flushSchedulerQueue
方法來 flush 隊(duì)列 queue
磷账,執(zhí)行它里面的所有 Watcher
對(duì)象的 run
方法。
flushSchedulerQueue
function flushSchedulerQueue () {
let watcher, id;
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
id = watcher.id;
has[id] = null;
watcher.run();
}
waiting = false;
}
舉個(gè)例子
let watch1 = new Watcher();
let watch2 = new Watcher();
watch1.update();
watch1.update();
watch2.update();
我們現(xiàn)在 new 了兩個(gè) Watcher
對(duì)象贾虽,因?yàn)樾薷牧?data
的數(shù)據(jù)逃糟,所以我們模擬觸發(fā)了兩次 watch1
的 update
以及 一次 watch2
的 update
。
假設(shè)沒有批量異步更新策略的話,理論上應(yīng)該執(zhí)行 Watcher
對(duì)象的 run
绰咽,那么會(huì)打印蛉抓。
watch1 update
watch1視圖更新啦~
watch1 update
watch1視圖更新啦~
watch2 update
watch2視圖更新啦~
實(shí)際上則執(zhí)行
watch1 update
watch1 update
watch2 update
watch1視圖更新啦~
watch2視圖更新啦~
這就是異步更新策略的效果,相同的 Watcher
對(duì)象會(huì)在這個(gè)過程中被剔除剃诅,在下一個(gè) tick 的時(shí)候去更新視圖,從而達(dá)到對(duì)我們第一個(gè)例子的優(yōu)化驶忌。
我們?cè)倩剡^頭聊一下第一個(gè)例子矛辕, number
會(huì)被不停地進(jìn)行 ++
操作,不斷地觸發(fā)它對(duì)應(yīng)的 Dep
中的 Watcher
對(duì)象的 update
方法付魔。然后最終 queue
中因?yàn)閷?duì)相同 id
的 Watcher
對(duì)象進(jìn)行了篩選聊品,從而 queue
中實(shí)際上只會(huì)存在一個(gè) number
對(duì)應(yīng)的 Watcher
對(duì)象。在下一個(gè) tick 的時(shí)候(此時(shí) number
已經(jīng)變成了 1000)几苍,觸發(fā) Watcher
對(duì)象的 run
方法來更新視圖翻屈,將視圖上的 number
從 0 直接變成 1000。
到這里妻坝,批量異步更新策略及 nextTick 原理已經(jīng)講完了伸眶,接下來讓我們學(xué)習(xí)一下 Vuex 狀態(tài)管理的工作原理。
注:本節(jié)代碼參考《批量異步更新策略及 nextTick 原理》刽宪。