Vue 源碼解析03-異步更新隊列
前言
這篇文章分析了Vue更新過程中使用的異步更新隊列的相關代碼。通過對異步更新隊列的研究和學習,加深對Vue更新機制的理解
什麼是異步更新隊列
先看看下面的例子:
<div id="app">
<div id="div" v-if="isShow">被隱藏的內容</div>
<input @click="getDiv" value="按鈕" type="button">
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
//控制是否顯示#div
isShow: false
},
methods:{
getDiv: function () {
this.isShow=true
var content = document.getElementById('div').innerHTML;
console.log('content',content)
}
}
})
</script>
- 上面的例子是,點擊按鈕顯示被隱藏的div,同時打印div內部html的內容。
- 按照我們一般的認知,應該是點擊按鈕能夠顯示div並且在控制檯看到div的內部html的內容。
但是實際執行的結果確是,div可以顯示出來,但是打印結果的時候會報錯,錯誤原因就是innerHTML爲null,也就是div不存在。
只有當我們再次點擊按鈕的時候纔會打印出div裏面的內容。這就是Vue的異步更新隊列的結果
異步更新隊列的概念
Vue的dom更新是異步的,當數據發生變化時Vue不是立刻去更新dom,而是開啓一個隊列,並緩衝在同一個事件中循環發生的所有數據變化。
在緩衝時,會去除重複的數據,避免多餘的計算和dom操作。在下一個事件循環tick中,刷新隊列並執行已去重的工作。
-
所以上面的代碼報錯是因爲當執行this.isShow=true時,div還未被創建出來,知道下次Vue事件循環時纔開始創建
-
查重機制降低了Vue的開銷
-
異步更新隊列實現的選擇:由於瀏覽器的差異,Vue會根據當前環境選擇Promise.then或者MuMutationObserver,如果兩者都不支持,則會用setImmediate或者setTimeout代替
異步更新隊列解析
異步隊列源碼入口
通過之前對Vue數據響應式的分析我們知道,當Vue數據發生變化時,會觸發dep的notify()方法,該方法通知觀察者watcher去更新dom,我們先看一下這的源碼
- from src/core/observer/dep.js
//直接看核心代碼
notify () {
//這是Dep的notify方法,Vue的會對data數據進行數據劫持,該方法被放到data數據的set方法中最後執行
//也就是通知更新操作
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
// !!!核心:通知watcher進行數據更新
//這裏的subs[i]其實是Dep維護的一個watcher數組,所以我們下面是執行的watcher中的update方法
subs[i].update()
}
}
- 上面的代碼簡單來說就是dep通知watcher盡心更新操作,我們看一下watcher相關的代碼
from :src/core/observer/watcher.js
//這裏只展示部分核心代碼
//watcher的update方法
update () {
/* istanbul ignore else */
//判斷是否存在lazy和sync屬性
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
//核心:將當前的watcher放到一個隊列中
queueWatcher(this)
}
}
- 上面watcher的update更新方法簡單來說就是調用了一個queueWatcher方法,這個方法其實是將當前的watcher實例放入到一個隊列中,以便完成後面的異步更新隊列操作
異步隊列入隊
下面看看queueWatcher的邏輯 from src/core/observer/scheduler.js
export function queueWatcher (watcher: Watcher) {
const 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.
let 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
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
// 啓動異步任務(刷新當前的計劃任務)
nextTick(flushSchedulerQueue)
}
}
}
- 上面這段queueWatcher的代碼的主要作用就是對任務去重,然後啓動異步任務,進行跟新操作。接下來我們看一線nextTick裏面的操作
from src/core/util/next-tick.js
//cb:
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
//callbacks:這個方法維護了一個回調函數的數組,將回調函數添家進數組
callbacks.push(() => {
//添加錯誤處理
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
//啓動異步函數
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
- 這裏的核心,其實就在timerFunc的函數上,該函數根據不同的運行時環境,調用不同的異步更新隊列,下面看一下代碼
from src/core/util/next-tick.js
/**這部分邏輯就是根據環境來判斷timerFunc到底是使用什麼樣的異步隊列**/
let timerFunc
//首選微任務執行異步操作:Promise、MutationObserver
//次選setImmediate最後選擇setTimeout
// 根據當前瀏覽器環境選擇用什麼方法來執行異步任務
if (typeof Promise !== 'undefined' && isNative(Promise)) {
//如果當前環境支持Promise,則使用Promise執行異步任務
const p = Promise.resolve()
timerFunc = () => {
//最終是執行的flushCallbacks方法
p.then(flushCallbacks)
//如果是IOS則回退,因爲IOS不支持Promise
if (isIOS) setTimeout(noop)
}
//當前使用微任務執行
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
//如果當前瀏覽器支持MutationObserver則使用MutationObserver
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
//如果支持setImmediate,則使用setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
//如果上面的條件都不滿足,那麼最後選擇setTimeout方法來完成異步更新隊列
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
- 從上面代碼可以看出,不論timerFunc使用的是什麼樣的異步更新隊列,最終執行的函數還是落在了flushCallbacks上面,那麼我們來看一看,這個方法到底是什麼
from src/core/util/next-tick.js
function flushCallbacks () {
pending = false
//拷貝callbacks數組內容
const copies = callbacks.slice(0)
//清空callbacks
callbacks.length = 0
//遍歷執行
for (let i = 0; i < copies.length; i++) {
//執行回調方法
copies[i]()
}
}
- 上面的這個方法就是遍歷執行了我們nextTick維護的那個回調函數數組,其實就是將數組的方法依次添加進異步隊列進行執行。同時清空callbacks數組爲下次更新作準備。
上面這幾段代碼其實都是watcher的異步隊列更新中的入隊操作,通過queueWatcher方法中調用的nextTick(flushSchedulerQueue),我們知道,其實是將flushSchedulerQueue這個方法入隊
異步隊列的具體更新方法
所以下面我們看一下flushSchedulerQueue這個方法到底執行了什麼操作
from src/core/observer/scheduler.js
/**我們這裏只粘貼跟本次異步隊列更新相關的核心代碼**/
//具體的更新操作
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
//重新排列queue數組,是爲了確保:
//更新順序是從父組件到子組件
//用戶的watcher先於render 的watcher執行(因爲用戶watcher先於render watcher創建)
//當子組件的watcher在父組件的watcher執行時被銷燬,則跳過該子組件的watcher
queue.sort((a, b) => a.id - b.id)
//queue數組維護的一個watcher數組
//遍歷queue數組,在queueWatcher方法中我們將傳入的watcher實例push到了該數組中
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
//清空has對象裏面的"id"屬性(這個id屬性之前在queueWatcher方法裏面查重的時候用到了)
has[id] = null
//核心:最終執行的其實是watcher的run方法
watcher.run()
//下面是一些警告提示,可以先忽略
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
//調用組件updated生命週期鉤子相關,先跳過
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
- 上面的一堆 flushSchedulerQueue 代碼,簡單來說就是排列了queue數組,然後遍歷該數組,執行watcher.run方法。所以,異步隊列更新當我們入隊完以後,真正執行的方法其實是watcher.run方法
下面我們來繼續看一下watcher.run方法,到底執行了什麼操作
from src/core/observer/watcher.js
/**
* Scheduler job interface.
* Will be called by the scheduler.
* 上面這段英文註釋 是官方註釋,從這我們看出該方法最終會被scheduler調用
*/
run () {
if (this.active) {
//這裏調用了watcher的get方法
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
- 上述run方法最終要的操作就是調用了watcher的get方法,該方法我們在之前的源碼分析有講過,主要實現的功能是調用了data數據的get方法,獲取最新數據。
至此,Vue異步更新隊列的核心代碼我們就分析完了,爲了便於理清思路,我們來一張圖總結一下
關於Vue.$nextTick
我們都知道nextTick** 方法就是直接調用的上面的nextTick方法
from src/core/instance/render.js
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
- 由上面的代碼我們可以看出,$nextTick是將我們傳入的回調函數加入到了異步更新隊列,所以它才能實現dom更新後回調
注意,$nextTick()是會將我們傳入的函數加入到異步更新隊列中的,但是這裏有個問題,如果我們想獲得dom更新後的數據,我們應該把該邏輯放到更新操作之後
因爲加入異步隊列先後的問題,如果我們在更新數據之前入隊的話 ,是獲取不到更新之後的數據的
總結
總結起來就是,當觸發數據更新通知時,dep通知watcher進行數據更新,這時watcher會將自己加入到一個異步的更新隊列中。然後更新隊列會將傳入的更新操作進行批量處理。
這樣就達到了多次更新同時完成,提升了用戶體驗,減少了瀏覽器的開銷,增強了性能。