在第三篇vue0.11版本源碼閱讀系列三:指令編譯里我們知道如果某個屬性的值變化了,會調用依賴該屬性的watcher
的update
方法:
p.update = function () {
if (!config.async || config.debug) {
this.run()
} else {
batcher.push(this)
}
}
它沒有直接調用指令的update
方法,而是交給了batcher
,本篇來看一下這個batcher
做了什么。
顧名思義,batcher
是批量的意思,所以就是批量更新,為什么要批量更新呢,先看一下下面的情況:
<div v-if="show">我出來了</div>
<div v-if="show && true">我也是</div>
window.vm.show = true
window.vm.show = false
比如有兩個指令依賴同一個屬性或者連續修改某個屬性,如果不進行批量異步更新,那么就會多次修改dom,這顯然是沒必要的,看下面兩個動圖能更直觀的感受到:
沒有進行批量異步更新的時候:
進行了批量異步更新:
能清晰的發現通過異步更新能跳過中間不必要的渲染以達到優化性能的效果。
接下來看一下具體實現,首先是push
函數:
// 定義了兩個隊列,一個用來存放用戶的watcher,一個用來存放指令更新的watcher
var queue = []
var userQueue = []
var has = {}
var waiting = false
var flushing = false
exports.push = function (job) {
// job就是watcher實例
var id = job.id
// 在沒有flushing的情況下has[id]用來跳過同一個watcher的重復添加
if (!id || !has[id] || flushing) {
has[id] = 1
// 首先要說明的是通過$watch方法或者watch選項生成的watcher代表是用戶的,user屬性為true
// 這里注釋說在執行任務中用戶的watcher可能會觸發非user的指令更新,所以要立即更新這個被觸發的指令,否則flushing這個變量是不需要的
if (flushing && !job.user) {
job.run()
return
}
// 根據指令的類型添加到不同的隊列里
;(job.user ? userQueue : queue).push(job)
// 上個隊列未被清空前不會創建新隊列
if (!waiting) {
waiting = true
_.nextTick(flush)
}
}
}
push
方法做的事情是把watcher
添加到隊列quene
里,然后如果沒有扔過flush
給nextTick
或者上次扔給nextTick
的flush
方法已經被執行了,就再給它一個。
flush
方法用來遍歷隊列里的watcher
并調用其run
方法,run
方法最終會調用指令的update
方法來更新頁面。
function flush () {
flushing = true
run(queue)
run(userQueue)
// 清空隊列和復位變量
reset()
}
function run (queue) {
// 循環執行watcher實例的run方法,run方法里會遍歷該watcher實例的指令隊列并執行指令的update方法
for (var i = 0; i < queue.length; i++) {
queue[i].run()
}
}
接下來就是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// 設為 true 表示監視指定目標節點或子節點樹中節點所包含的字符數據的變化
})
timerFunc = function () {
counter = (counter + 1) % 2// counter會在0和1兩者循環變化
textNode.data = counter// 節點變化會觸發回調handle,
}
} else {// 否則使用定時器
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)
}
})()
這是個自執行函數,一般用來定義并保存一些局部變量,返回了一個函數,就是nextTick
方法本法了,flush
方法會被push
到callbacks
數組里,我們常用的方法this.$nextTick(() => {xxxx})
也會把回調添加到這個數組里,這里也有一個變量pending
來控制重復添加的問題,最后添加到事件循環的隊列里的是handle
方法。
批量很容易理解,都放到一個隊列里,最后一起執行就是批量執行了,但是要理解MutationObserver
的回調或者setTimeout
的回調為什么能異步調用就需要先來了解一下JavaScript
語言里的事件循環Event Loop
的原理了。
簡單的說就是因為JavaScript
是單線程的,所以任務需要排隊進行執行,前一個執行完了才能執行后面一個,但有些任務比較耗時而且沒必要等著,所以可以先放一邊,先執行后面的,等到了可以執行了再去執行它,比如有些IO
操作,像常見的鼠標鍵盤事件注冊、Ajax
請求、settimeout
定時器、Promise
回調等。所以會存在兩個隊列,一個是同步隊列,也就是主線程,另一個是異步隊列,剛才提到的那些事件的回調如果可以被執行了都會被放在異步隊列里,當主線程上的任務執行完畢后會把異步隊列的任務取過來進行執行,所以同步代碼總是在異步代碼之前執行,執行完了后又會去檢查異步隊列,這樣不斷循環就是Event Loop
。
但是異步任務里其實還是分兩種,一種叫宏任務,常見的為:setTimeout
、setInterval
,另一種叫微任務,常見的如:Promise
、MutationObserver
。微任務會在宏任務之前執行,即使宏任務的回調先被添加到隊列里。
現在可以來分析一下異步更新的原理,就以開頭提到的例子來說:
<div v-if="show">我出來了</div>
<div v-if="show && true">我也是</div>
window.vm.show = true
window.vm.show = false
因為有兩個指令都依賴了show
,表達式不一樣,所以會有兩個watcher
,這兩個watcher
都會被show
屬性的dep
收集,所以每修改一次show
的值都會觸發這兩個watcher
的更新,也就是會調兩次batcher.push(this)
方法,第一次調用后會執行_.nextTick(flush)
注冊一個回調,連續兩次修改show
的值,會調用四次上述提到的batcher.push(this)
方法,因為重復添加的被過濾掉了,所以最后會有兩個watcher
被添加到隊列里,以上這些操作都是同步任務,所以是連續被執行完的,等這些同步任務都被執行完了后就會把剛才注冊的回調handle
拿過來執行,也就是會一次性執行剛才添加的兩個watcher
:
以上就是vue
異步更新的全部內容。