在第三篇vue0.11版本源碼閱讀系列三:指令編譯里我們知道如果某個(gè)屬性的值變化了,會(huì)調(diào)用依賴(lài)該屬性的watcher的update方法:
p.update = function () {
if (!config.async || config.debug) {
this.run()
} else {
batcher.push(this)
}
}
它沒(méi)有直接調(diào)用指令的update方法,而是交給了batcher,本篇來(lái)看一下這個(gè)batcher做了什么。
顧名思義,batcher是批量的意思,所以就是批量更新,為什么要批量更新呢,先看一下下面的情況:
<div v-if="show">我出來(lái)了</div>
<div v-if="show && true">我也是</div>
window.vm.show = true
window.vm.show = false
比如有兩個(gè)指令依賴(lài)同一個(gè)屬性或者連續(xù)修改某個(gè)屬性,如果不進(jìn)行批量異步更新,那么就會(huì)多次修改dom,這顯然是沒(méi)必要的,看下面兩個(gè)動(dòng)圖能更直觀的感受到:
沒(méi)有進(jìn)行批量異步更新的時(shí)候:
進(jìn)行了批量異步更新:

能清晰的發(fā)現(xiàn)通過(guò)異步更新能跳過(guò)中間不必要的渲染以達(dá)到優(yōu)化性能的效果。
接下來(lái)看一下具體實(shí)現(xiàn),首先是push函數(shù):
// 定義了兩個(gè)隊(duì)列,一個(gè)用來(lái)存放用戶(hù)的watcher,一個(gè)用來(lái)存放指令更新的watcher
var queue = []
var userQueue = []
var has = {}
var waiting = false
var flushing = false
exports.push = function (job) {
// job就是watcher實(shí)例
var id = job.id
// 在沒(méi)有flushing的情況下has[id]用來(lái)跳過(guò)同一個(gè)watcher的重復(fù)添加
if (!id || !has[id] || flushing) {
has[id] = 1
// 首先要說(shuō)明的是通過(guò)$watch方法或者watch選項(xiàng)生成的watcher代表是用戶(hù)的,user屬性為true
// 這里注釋說(shuō)在執(zhí)行任務(wù)中用戶(hù)的watcher可能會(huì)觸發(fā)非user的指令更新,所以要立即更新這個(gè)被觸發(fā)的指令,否則flushing這個(gè)變量是不需要的
if (flushing && !job.user) {
job.run()
return
}
// 根據(jù)指令的類(lèi)型添加到不同的隊(duì)列里
;(job.user ? userQueue : queue).push(job)
// 上個(gè)隊(duì)列未被清空前不會(huì)創(chuàng)建新隊(duì)列
if (!waiting) {
waiting = true
_.nextTick(flush)
}
}
}
push方法做的事情是把watcher添加到隊(duì)列quene里,然后如果沒(méi)有扔過(guò)flush給nextTick或者上次扔給nextTick的flush方法已經(jīng)被執(zhí)行了,就再給它一個(gè)。
flush方法用來(lái)遍歷隊(duì)列里的watcher并調(diào)用其run方法,run方法最終會(huì)調(diào)用指令的update方法來(lái)更新頁(yè)面。
function flush () {
flushing = true
run(queue)
run(userQueue)
// 清空隊(duì)列和復(fù)位變量
reset()
}
function run (queue) {
// 循環(huán)執(zhí)行watcher實(shí)例的run方法,run方法里會(huì)遍歷該watcher實(shí)例的指令隊(duì)列并執(zhí)行指令的update方法
for (var i = 0; i < queue.length; i++) {
queue[i].run()
}
}
接下來(lái)就是nextTick方法了:
exports.nextTick = (function () {
var callbacks = []
var pending = false
var timerFunc
function handle () {
pending = false
var copies = callbacks.slice(0)
callbacks = []
for (var i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 支持MutationObserver接口的話使用MutationObserver
if (typeof MutationObserver !== 'undefined') {
var counter = 1
var observer = new MutationObserver(handle)
var textNode = document.createTextNode(counter)
observer.observe(textNode, {
characterData: true// 設(shè)為 true 表示監(jiān)視指定目標(biāo)節(jié)點(diǎn)或子節(jié)點(diǎn)樹(shù)中節(jié)點(diǎn)所包含的字符數(shù)據(jù)的變化
})
timerFunc = function () {
counter = (counter + 1) % 2// counter會(huì)在0和1兩者循環(huán)變化
textNode.data = counter// 節(jié)點(diǎn)變化會(huì)觸發(fā)回調(diào)handle,
}
} else {// 否則使用定時(shí)器
timerFunc = setTimeout
}
return function (cb, ctx) {
var func = ctx
? function () { cb.call(ctx) }
: cb
callbacks.push(func)
if (pending) return
pending = true
timerFunc(handle, 0)
}
})()
這是個(gè)自執(zhí)行函數(shù),一般用來(lái)定義并保存一些局部變量,返回了一個(gè)函數(shù),就是nextTick方法本法了,flush方法會(huì)被push到callbacks數(shù)組里,我們常用的方法this.$nextTick(() => {xxxx})也會(huì)把回調(diào)添加到這個(gè)數(shù)組里,這里也有一個(gè)變量pending來(lái)控制重復(fù)添加的問(wèn)題,最后添加到事件循環(huán)的隊(duì)列里的是handle方法。
批量很容易理解,都放到一個(gè)隊(duì)列里,最后一起執(zhí)行就是批量執(zhí)行了,但是要理解MutationObserver的回調(diào)或者setTimeout的回調(diào)為什么能異步調(diào)用就需要先來(lái)了解一下JavaScript語(yǔ)言里的事件循環(huán)Event Loop的原理了。
簡(jiǎn)單的說(shuō)就是因?yàn)?code>JavaScript是單線程的,所以任務(wù)需要排隊(duì)進(jìn)行執(zhí)行,前一個(gè)執(zhí)行完了才能執(zhí)行后面一個(gè),但有些任務(wù)比較耗時(shí)而且沒(méi)必要等著,所以可以先放一邊,先執(zhí)行后面的,等到了可以執(zhí)行了再去執(zhí)行它,比如有些IO操作,像常見(jiàn)的鼠標(biāo)鍵盤(pán)事件注冊(cè)、Ajax請(qǐng)求、settimeout定時(shí)器、Promise回調(diào)等。所以會(huì)存在兩個(gè)隊(duì)列,一個(gè)是同步隊(duì)列,也就是主線程,另一個(gè)是異步隊(duì)列,剛才提到的那些事件的回調(diào)如果可以被執(zhí)行了都會(huì)被放在異步隊(duì)列里,當(dāng)主線程上的任務(wù)執(zhí)行完畢后會(huì)把異步隊(duì)列的任務(wù)取過(guò)來(lái)進(jìn)行執(zhí)行,所以同步代碼總是在異步代碼之前執(zhí)行,執(zhí)行完了后又會(huì)去檢查異步隊(duì)列,這樣不斷循環(huán)就是Event Loop。
但是異步任務(wù)里其實(shí)還是分兩種,一種叫宏任務(wù),常見(jiàn)的為:setTimeout、setInterval,另一種叫微任務(wù),常見(jiàn)的如:Promise、MutationObserver。微任務(wù)會(huì)在宏任務(wù)之前執(zhí)行,即使宏任務(wù)的回調(diào)先被添加到隊(duì)列里。
現(xiàn)在可以來(lái)分析一下異步更新的原理,就以開(kāi)頭提到的例子來(lái)說(shuō):
<div v-if="show">我出來(lái)了</div>
<div v-if="show && true">我也是</div>
window.vm.show = true
window.vm.show = false
因?yàn)橛袃蓚€(gè)指令都依賴(lài)了show,表達(dá)式不一樣,所以會(huì)有兩個(gè)watcher,這兩個(gè)watcher都會(huì)被show屬性的dep收集,所以每修改一次show的值都會(huì)觸發(fā)這兩個(gè)watcher的更新,也就是會(huì)調(diào)兩次batcher.push(this)方法,第一次調(diào)用后會(huì)執(zhí)行_.nextTick(flush)注冊(cè)一個(gè)回調(diào),連續(xù)兩次修改show的值,會(huì)調(diào)用四次上述提到的batcher.push(this)方法,因?yàn)橹貜?fù)添加的被過(guò)濾掉了,所以最后會(huì)有兩個(gè)watcher被添加到隊(duì)列里,以上這些操作都是同步任務(wù),所以是連續(xù)被執(zhí)行完的,等這些同步任務(wù)都被執(zhí)行完了后就會(huì)把剛才注冊(cè)的回調(diào)handle拿過(guò)來(lái)執(zhí)行,也就是會(huì)一次性執(zhí)行剛才添加的兩個(gè)watcher:

以上就是vue異步更新的全部?jī)?nèi)容。