Vue 源碼剖析2
異步更新隊列
Vue 高效的秘訣是一套批量、異步的更新策略
概念解釋
事件循環(huán) Event Loop:瀏覽器為了協(xié)調(diào)事件處理、腳本執(zhí)行、網(wǎng)絡(luò)請求和渲染等任務(wù)而制定的工作機制。
宏任務(wù) Task:代表一個個離散的羞迷、獨立的工作單元。瀏覽器完成一個宏任務(wù)画饥,在下一個宏任務(wù)執(zhí)行開始前衔瓮,會對頁面進行重新渲染。主要包括創(chuàng)建文檔對象抖甘、解析 HTML热鞍、執(zhí)行主線 JS 代碼以及各種事件如頁面加載、輸入衔彻、網(wǎng)絡(luò)事件和定時器等薇宠。
微任務(wù):微任務(wù)是更小的任務(wù),是在當前宏任務(wù)執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)米奸。如果存在微任務(wù)昼接,瀏覽器會清空微任務(wù)之后再重新渲染爽篷。微任務(wù)的例子有 Promise 回調(diào)函數(shù)悴晰、DOM 變化等。
Vue 中的具體實現(xiàn)
異步:只要偵聽到數(shù)據(jù)變化,Vue 將開啟一個隊列铡溪,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更漂辐。
批量:如果同一個 Watcher 被多次觸發(fā),只會被推入到隊列中一次棕硫。去重對于避免不必要的計算和 DOM 操作是非常重要的髓涯。然后,在下一個的事件循環(huán) tick 中哈扮,Vue 刷新隊列執(zhí)行實際工作纬纪。
異步策略:Vue 在內(nèi)部對異步隊列嘗試使用原生的 Promise.then、MutationObserver 或 setTmmediate滑肉,如果執(zhí)行環(huán)境都不支持包各,則會采用 setTimeout 代替。
update() core\observer\watcher.js
dep.notify() 之后 watcher 執(zhí)行更新靶庙,執(zhí)行入隊操作
queueWatcher(watcher) core\observer\scheduler.js
執(zhí)行 watcher 入隊操作
nextTick(flushSchedulerQueue) core\util\next-tick.js
nextTick 按照特定異步策略執(zhí)行隊列操作
測試代碼:03-timerFunc.html
watcher 中 update 執(zhí)行三次问畅,但 run 僅執(zhí)行一次,且數(shù)值變化對 dom 的影響也不是立竿見影的六荒。
可以研究下相關(guān) API:vm.$nextTick(cb)
$nextTick 把傳入回調(diào)函數(shù)放入 callbacks 隊尾
$nextTick 原理執(zhí)行順序:
Promise==>MutationObserver==>SetImmediate==>setTimeout
虛擬 DOM
概念
虛擬 DOM(Vitual DOM)是對 DOM 的 JS 抽象表示护姆,他們是 JS 對象,能夠描述 DOM 結(jié)構(gòu)和關(guān)系掏击。應(yīng)用的各種狀態(tài)變化會作用于虛擬 DOM卵皂,最終映射到 DOM 上。
體驗虛擬 DOM
Vue 中虛擬 dom 基于 snabbdom 實現(xiàn)砚亭,安裝 snabbdom 并體驗
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>
<div id="app"></div>
<!--安裝并引?snabbdom-->
<script src="../../node_modules/snabbdom/dist/snabbdom.js"></script>
<script>
// 之前編寫的響應(yīng)式函數(shù)
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
val = newVal
// 通知更新
update()
}
})
}
// 導?patch的??init渐裂,h是產(chǎn)?vnode的??
const { init, h } = snabbdom
// 獲取patch函數(shù)
const patch = init([])
// 上次vnode,由patch()返回
let vnode;
// 更新函數(shù)钠惩,將數(shù)據(jù)操作轉(zhuǎn)換為dom操作柒凉,返回新vnode
function update() {
if (!vnode) {
// 初始化,沒有上次vnode篓跛,傳?宿主元素和vnode
vnode = patch(app, render())
}
else {
// 更新膝捞,傳?新舊vnode對?并做更新
vnode = patch(vnode, render())
}
}
// 渲染函數(shù),返回vnode描述dom結(jié)構(gòu)
function render() {
return h('div', obj.foo)
}
// 數(shù)據(jù)
const obj = {}
// 定義響應(yīng)式
defineReactive(obj, 'foo', '')
// 賦?個?期作為初始值
obj.foo = new Date().toLocaleTimeString()
// 定時改變數(shù)據(jù)愧沟,更新函數(shù)會重新執(zhí)?
setInterval(() => {
obj.foo = new Date().toLocaleTimeString()
}, 1000);
</script>
</body>
</html>
優(yōu)點
- 虛擬 DOM 輕量蔬咬、快速:當它們發(fā)生變化時通過新舊虛擬 DOM 比對可以得到最小 DOM 操作量,配合異步更新策略減少刷新頻率沐寺,從而提升性能
patch(vnode, h('div', obj.foo))
- 跨平臺:將虛擬 dom 更新轉(zhuǎn)換為不同運行時特殊操作實現(xiàn)跨平臺
<script src="../../node_modules/snabbdom/dist/snabbdom-style.js"></script>
<script>
// 增加style模塊
const patch = init([snabbdom_style.default])
function render() {
// 添加節(jié)點樣式描述
return h('div', {style: {color: 'red' } }, obj.foo)
}
</script>
- 兼容性:還可以加入兼容性代碼增強操作的兼容性
必要性
vue 1.0 中有細粒度的數(shù)據(jù)變化偵測林艘,它是不需要虛擬 DOM 的,但是細粒度造成了大量開銷混坞,這對于大型項目來說是不可接受的狐援。因此钢坦,vue 2.0 選擇了中等粒度的解決方案,每?個組件?個 watcher 實例啥酱,這樣狀態(tài)變化時只能通知到組件爹凹,再通過引入虛擬 DOM 去進行比對和渲染。
整體流程
mountComponent() core/instance/lifecycle.js
渲染镶殷、更新組件
// 定義更新函數(shù)
const updateComponent = () => {
// 實際調(diào)?是在lifeCycleMixin中定義的_update和renderMixin中定義的_render
vm._update(vm._render(), hydrating)
}
_render core/instance/render.js
生成虛擬 dom
_update core\instance\lifecycle.js
update 負責更新 dom禾酱,轉(zhuǎn)換 vnode 為 dom
patch() platforms/web/runtime/index.js
patch是在平臺特有代碼中指定的
Vue.prototype.__patch__ = inBrowser ? patch : noop
測試代碼,examples\test\04-vdom.html
patch
patch 獲取
patch 是 createPatchFunction 的返回值绘趋,傳遞 nodeOps 和 modules 是 web 平臺特別實現(xiàn)
export const patch: Function = createPatchFunction({ nodeOps, modules })
platforms\web\runtime\node-ops.js
定義各種原生 dom 基礎(chǔ)操作方法
platforms\web\runtime\modules\index.js
modules 定義了屬性更新實現(xiàn)
watcher.run() => componentUpdate() => render() => update() => patch()
patch 實現(xiàn)
patch core\vdom\patch.js
首先進行樹級別比較颤陶,可能有三種情況:增刪改。
new VNode 不存在就刪陷遮;
old VNode 不存在就增指郁;
都存在就執(zhí)行 diff 執(zhí)行更新
patchVnode
比較兩個 VNode,包括三種類型操作:屬性更新拷呆、文本更新闲坎、子節(jié)點更新
具體規(guī)則如下:
新老節(jié)點均有 children 子節(jié)點,則對子節(jié)點進行 diff 操作茬斧,調(diào)用 updateChildren
如果新節(jié)點有子節(jié)點點而老節(jié)點沒有子節(jié)點腰懂,先清空老節(jié)點的文本內(nèi)容,然后為其新增子節(jié)點
當新節(jié)點沒有子節(jié)點而老節(jié)點有子節(jié)點的時候项秉,則移除該節(jié)點的所有子節(jié)點
當新老節(jié)點都無子節(jié)點的時候绣溜,只是文本的替換
測試,04-vdom.html
// patchVnode過程分解
// 1.div#demo updateChildren
// 2.h1 updateChildren
// 3.text ?本相同跳過
// 4.p updateChildren
// 5.text setTextContent
updateChildren
updateChildren 主要作用是用?種較高效的方式比對新舊兩個 VNode 的 children 得出最小操作補丁娄蔼。執(zhí)行?個雙循環(huán)是傳統(tǒng)方式怖喻,Vue 中針對 web 場景特點做了特別的算法優(yōu)化,我們看圖說話:
在新老兩組 VNode 節(jié)點的左右頭尾兩側(cè)都有?個變量標記岁诉,在遍歷過程中這幾個變量都會向中間靠攏锚沸。
當oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx時結(jié)束循環(huán)。
下面是遍歷規(guī)則:
首先涕癣,oldStartVnode哗蜈、oldEndVnode與newStartVnode、newEndVnode 兩兩交叉比較坠韩,共有4種比較方法距潘。
當 oldStartVnode 和 newStartVnode 或者 oldEndVnode 和 newEndVnode 滿足 sameVnode,直接將該 VNode 節(jié)點進行 patchVnode 即可只搁,不需再遍歷就完成了?次循環(huán)音比。如下圖:
如果 oldStartVnode 與 newEndVnode 滿足 sameVnode。說明 oldStartVnode 已經(jīng)跑到了 oldEndVnode 后面去了氢惋,進行 patchVnode 的同時還需要將真實 DOM 節(jié)點移動到 oldEndVnode 的后面洞翩。
如果 oldEndVnode 與 newStartVnode 滿足 sameVnode稽犁,說明 oldEndVnode 跑到了 oldStartVnode 的前面,進行 patchVnode 的同時要將 oldEndVnode 對應(yīng) DOM 移動到 oldStartVnode 對應(yīng) DOM 的前面菱农。
如果以上情況均不符合,則在 old VNode 中找與 newStartVnode 相同的節(jié)點柿估,若存在執(zhí)行 patchVnode循未,同時將 elmToMove 移動到 oldStartIdx 對應(yīng)的 DOM 的前面。
當然也有可能 newStartVnode 在 old VNode 節(jié)點中找不到?致的 sameVnode秫舌,這個時候會調(diào)用 createElm 創(chuàng)建?個新的 DOM 節(jié)點的妖。
至此循環(huán)結(jié)束,但是我們還需要處理剩下的節(jié)點足陨。
當結(jié)束時 oldStartIdx > oldEndIdx嫂粟,這個時候舊的 VNode 節(jié)點已經(jīng)遍歷完了,但是新的節(jié)點還沒有墨缘。說明了新的 VNode 節(jié)點實際上比老的 VNode 節(jié)點多星虹,需要將剩下的 VNode 對應(yīng)的 DOM 插入到真實 DOM 中,此時調(diào)用 addVnodes(批量調(diào)用 createElm 接口)镊讼。
但是宽涌,當結(jié)束時 newStartIdx > newEndIdx 時,說明新的 VNode 節(jié)點已經(jīng)遍歷完了蝶棋,但是老的節(jié)點還有剩余卸亮,需要從文檔中將老的節(jié)點刪除。
總結(jié)&&思考
const app = new Vue({
el: '#demo',
data: { foo: 'ready~~' },
mounted () {
// 批量玩裙、異步
// 每次賦值兼贸,watcher入隊
// $nextTick()把傳入回調(diào)函數(shù)放入callbacks隊尾
this.foo = Math.random()
console.log('1:' + this.foo);
this.foo = Math.random()
console.log('2:' + this.foo);
this.foo = Math.random()
console.log('3:' + this.foo);
// 異步行為,此時內(nèi)容沒變
console.log('p1.innerHTML:' + p1.innerHTML) // ready~~
// [callbacks, fn]
// Promise.resolve().then(() => {
// console.log('promise, p1.innerHTML:' + p1.innerHTML)
// })
this.$nextTick(() => {
// 這里才是最新的值
console.log('p1.innerHTML:' + p1.innerHTML)
})
}
});
面試官:在 Vue 里面執(zhí)行 mounted 里面的內(nèi)容吃溅,輸出結(jié)果如何溶诞?
面試官:如果把 $nextTick 放在中間位置呢?
面試官:如果把 $nextTick 放在最上面位置呢决侈?
面試官:如果再加上 Promise 呢很澄?
如果...
如果沒有如果...
附思維導圖: