vue是現(xiàn)在主流前端框架之一,采用了很多高級(jí)特性威酒,如虛擬DOM窑睁,那么它是如何批量更新的,我們一起來了解下葵孤。
數(shù)據(jù)變化担钮,如何更新dom
DOM“天生就慢”,所以前端各大框架都提供了對(duì)DOM操作進(jìn)行優(yōu)化的辦法尤仍,Angular中的是臟值檢查箫津,React首先提出了Virtual Dom,Vue2.0也加入了Virtual Dom宰啦,與React類似苏遥。
要知道渲染真實(shí)DOM的開銷是很大的,比如有時(shí)候我們修改了某個(gè)數(shù)據(jù)赡模,如果直接渲染到真實(shí)dom上會(huì)引起整個(gè)dom樹的重繪和重排田炭,有沒有可能我們只更新我們修改的那一小塊dom而不要更新整個(gè)dom呢?diff算法能夠幫助我們纺裁。
我們先根據(jù)真實(shí)DOM生成一顆virtual DOM诫肠,當(dāng)virtual DOM某個(gè)節(jié)點(diǎn)的數(shù)據(jù)改變后會(huì)生成一個(gè)新的Vnode,然后Vnode和oldVnode作對(duì)比欺缘,發(fā)現(xiàn)有不一樣的地方就直接修改在真實(shí)的DOM上栋豫,然后使oldVnode的值為Vnode。
diff的過程就是調(diào)用名為patch的函數(shù)谚殊,比較新舊節(jié)點(diǎn)丧鸯,一邊比較一邊給真實(shí)的DOM打補(bǔ)丁。
虛擬dom與真實(shí)dom
真實(shí)dom
<div>
<p>111</p>
</div>
對(duì)應(yīng)的virtual DOM(偽代碼)
var Vnode = {
tag: 'div',
children: [
{ tag: 'p', text: '111' }
]
};
diff對(duì)比方式
在采取diff算法比較新舊節(jié)點(diǎn)的時(shí)候嫩絮,比較只會(huì)在同層級(jí)進(jìn)行, 不會(huì)跨層級(jí)比較丛肢。
<div>
<p>123</p>
</div>
<div>
<span>456</span>
</div>
上面的代碼會(huì)分別比較同一層的兩個(gè)div以及第二層的p和span,但是不會(huì)拿div和span作比較剿干。在別處看到的一張很形象的圖:
diff流程圖
當(dāng)數(shù)據(jù)發(fā)生改變時(shí)蜂怎,set方法會(huì)讓調(diào)用Dep.notify通知所有訂閱者Watcher,訂閱者就會(huì)調(diào)用patch給真實(shí)的DOM打補(bǔ)丁置尔,更新相應(yīng)的視圖杠步。
VNode對(duì)象
一個(gè)VNode的實(shí)例包含了以下屬性,這部分代碼在src/core/vdom/vnode.js里
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
functionalContext: Component | void; // real context vm for functional nodes
functionalOptions: ?ComponentOptions; // for SSR caching
functionalScopeId: ?string; // functioanl scope id support
- tag: 當(dāng)前節(jié)點(diǎn)的標(biāo)簽名
- data: 當(dāng)前節(jié)點(diǎn)的數(shù)據(jù)對(duì)象榜轿,具體包含哪些字段可以參考vue源碼types/vnode.d.ts中對(duì)VNodeData的定義
- children: 數(shù)組類型幽歼,包含了當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)
- text: 當(dāng)前節(jié)點(diǎn)的文本,一般文本節(jié)點(diǎn)或注釋節(jié)點(diǎn)會(huì)有該屬性
- elm: 當(dāng)前虛擬節(jié)點(diǎn)對(duì)應(yīng)的真實(shí)的dom節(jié)點(diǎn)
- ns: 節(jié)點(diǎn)的namespace
- context: 編譯作用域
- functionalContext: 函數(shù)化組件的作用域
- key: 節(jié)點(diǎn)的key屬性谬盐,用于作為節(jié)點(diǎn)的標(biāo)識(shí)甸私,有利于patch的優(yōu)化
- componentOptions: 創(chuàng)建組件實(shí)例時(shí)會(huì)用到的選項(xiàng)信息
- child: 當(dāng)前節(jié)點(diǎn)對(duì)應(yīng)的組件實(shí)例
- parent: 組件的占位節(jié)點(diǎn)
- raw: raw html
- isStatic: 靜態(tài)節(jié)點(diǎn)的標(biāo)識(shí)
- isRootInsert: 是否作為根節(jié)點(diǎn)插入,被包裹的節(jié)點(diǎn)飞傀,該屬性的值為false
- isComment: 當(dāng)前節(jié)點(diǎn)是否是注釋節(jié)點(diǎn)
- isCloned: 當(dāng)前節(jié)點(diǎn)是否為克隆節(jié)點(diǎn)
- isOnce: 當(dāng)前節(jié)點(diǎn)是否有v-once指令
VNode的分類
VNode可以理解為VueVirtual Dom的一個(gè)基類皇型,通過VNode構(gòu)造函數(shù)生成的VNnode實(shí)例可為如下幾類:
- EmptyVNode: 沒有內(nèi)容的注釋節(jié)點(diǎn)
- TextVNode: 文本節(jié)點(diǎn)
- ElementVNode: 普通元素節(jié)點(diǎn)
- ComponentVNode: 組件節(jié)點(diǎn)
- CloneVNode: 克隆節(jié)點(diǎn),可以是以上任意類型的節(jié)點(diǎn)砸烦,唯一的區(qū)別在于isCloned屬性為true
具體Diff分析
來看看patch是怎么打補(bǔ)丁的(代碼只保留核心部分)
function patch (oldVnode, vnode) {
// some code
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el // 當(dāng)前oldVnode對(duì)應(yīng)的真實(shí)元素節(jié)點(diǎn)
let parentEle = api.parentNode(oEl) // 父元素
createEle(vnode) // 根據(jù)Vnode生成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 將新元素添加進(jìn)父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的舊元素節(jié)點(diǎn)
oldVnode = null
}
}
// some code
return vnode
}
patch函數(shù)接收兩個(gè)參數(shù)oldVnode和Vnode分別代表新的節(jié)點(diǎn)和之前的舊節(jié)點(diǎn)
判斷兩節(jié)點(diǎn)是否值得比較犀被,值得比較則執(zhí)行patchVnode
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 標(biāo)簽名
a.isComment === b.isComment && // 是否為注釋節(jié)點(diǎn)
// 是否都定義了data,data包含一些具體信息外冀,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 當(dāng)標(biāo)簽是<input>的時(shí)候寡键,type必須相同
)
}
不值得比較則用Vnode替換oldVnode
如果兩個(gè)節(jié)點(diǎn)都是一樣的,那么就深入檢查他們的子節(jié)點(diǎn)雪隧。如果兩個(gè)節(jié)點(diǎn)不一樣那就說明Vnode完全被改變了儒将,就可以直接替換oldVnode。
雖然這兩個(gè)節(jié)點(diǎn)不一樣但是他們的子節(jié)點(diǎn)一樣怎么辦讯柔?別忘了蝎困,diff可是逐層比較的,如果第一層不一樣那么就不會(huì)繼續(xù)深入比較第二層了庄拇。
patchVnode
當(dāng)我們確定兩個(gè)節(jié)點(diǎn)值得比較之后我們會(huì)對(duì)兩個(gè)節(jié)點(diǎn)指定patchVnode方法注服。那么這個(gè)方法做了什么呢韭邓?
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){
createEle(vnode) //create el's children dom
}else if (oldCh){
api.removeChildren(el)
}
}
}
這個(gè)函數(shù)做了以下事情:
- 找到對(duì)應(yīng)的真實(shí)dom,稱為el
- 判斷Vnode和oldVnode是否指向同一個(gè)對(duì)象溶弟,如果是女淑,那么直接return
- 如果他們都有文本節(jié)點(diǎn)并且不相等,那么將el的文本節(jié)點(diǎn)設(shè)置為Vnode的文本節(jié)點(diǎn)辜御。
- 如果oldVnode有子節(jié)點(diǎn)而Vnode沒有鸭你,則刪除el的子節(jié)點(diǎn)
- 如果oldVnode沒有子節(jié)點(diǎn)而Vnode有,則將Vnode的子節(jié)點(diǎn)真實(shí)化之后添加到el
- 如果兩者都有子節(jié)點(diǎn)擒权,則執(zhí)行updateChildren函數(shù)比較子節(jié)點(diǎn)袱巨,這一步很重要
其他幾個(gè)點(diǎn)都很好理解,我們?cè)敿?xì)來講一下updateChildren
updateChildren
updataChildren是Diff算法的核心碳抄,所以本文對(duì)updataChildren進(jìn)行了圖文的分析愉老。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // 舊頭索引
let newStartIdx = 0 // 新頭索引
let oldEndIdx = oldCh.length - 1 // 舊尾索引
let newEndIdx = newCh.length - 1 // 新尾索引
let oldStartVnode = oldCh[0] // oldVnode的第一個(gè)child
let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一個(gè)child
let newStartVnode = newCh[0] // newVnode的第一個(gè)child
let newEndVnode = newCh[newEndIdx] // newVnode的最后一個(gè)child
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
// 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了剖效,證明diff完了俺夕,循環(huán)結(jié)束
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果oldVnode的第一個(gè)child不存在
if (isUndef(oldStartVnode)) {
// oldStart索引右移
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
// 如果oldVnode的最后一個(gè)child不存在
} else if (isUndef(oldEndVnode)) {
// oldEnd索引左移
oldEndVnode = oldCh[--oldEndIdx]
// oldStartVnode和newStartVnode是同一個(gè)節(jié)點(diǎn)
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// patch oldStartVnode和newStartVnode, 索引左移贱鄙,繼續(xù)循環(huán)
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldEndVnode和newEndVnode是同一個(gè)節(jié)點(diǎn)
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// patch oldEndVnode和newEndVnode劝贸,索引右移,繼續(xù)循環(huán)
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldStartVnode和newEndVnode是同一個(gè)節(jié)點(diǎn)
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// patch oldStartVnode和newEndVnode
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 如果removeOnly是false逗宁,則將oldStartVnode.eml移動(dòng)到oldEndVnode.elm之后
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// oldStart索引右移映九,newEnd索引左移
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 如果oldEndVnode和newStartVnode是同一個(gè)節(jié)點(diǎn)
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// patch oldEndVnode和newStartVnode
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 如果removeOnly是false,則將oldEndVnode.elm移動(dòng)到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// oldEnd索引左移瞎颗,newStart索引右移
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 如果都不匹配
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 嘗試在oldChildren中尋找和newStartVnode的具有相同的key的Vnode
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果未找到件甥,說明newStartVnode是一個(gè)新的節(jié)點(diǎn)
if (isUndef(idxInOld)) { // New element
// 創(chuàng)建一個(gè)新Vnode
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
// 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
} else {
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
// 比較兩個(gè)具有相同的key的新節(jié)點(diǎn)是否是同一個(gè)節(jié)點(diǎn)
//不設(shè)key哼拔,newCh和oldCh只會(huì)進(jìn)行頭尾兩端的相互比較引有,設(shè)key后,除了頭尾兩端的比較外倦逐,還會(huì)從用key生成的對(duì)象oldKeyToIdx中查找匹配的節(jié)點(diǎn)譬正,所以為節(jié)點(diǎn)設(shè)置key可以更高效的利用dom。
if (sameVnode(vnodeToMove, newStartVnode)) {
// patch vnodeToMove和newStartVnode
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
// 清除
oldCh[idxInOld] = undefined
// 如果removeOnly是false檬姥,則將找到的和newStartVnodej具有相同的key的Vnode曾我,叫vnodeToMove.elm
// 移動(dòng)到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
// 如果key相同,但是節(jié)點(diǎn)不相同健民,則創(chuàng)建一個(gè)新的節(jié)點(diǎn)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
}
// 右移
newStartVnode = newCh[++newStartIdx]
}
}
先說一下這個(gè)函數(shù)做了什么
將Vnode的子節(jié)點(diǎn)Vch和oldVnode的子節(jié)點(diǎn)oldCh提取出來
oldCh和vCh各有兩個(gè)頭尾的變量StartIdx和EndIdx抒巢,它們的2個(gè)變量相互比較,一共有4種比較方式秉犹。如果4種比較都沒匹配蛉谜,如果設(shè)置了key稚晚,就會(huì)用key進(jìn)行比較,在比較的過程中型诚,變量會(huì)往中間靠客燕,一旦StartIdx>EndIdx表明oldCh和vCh至少有一個(gè)已經(jīng)遍歷完了,就會(huì)結(jié)束比較俺驶。
圖解updateChildren
終于來到了這一部分幸逆,上面的總結(jié)相信很多人也看得一臉懵逼棍辕,下面我們好好說道說道暮现。(這都是我自己畫的,求推薦好用的畫圖工具...)
粉紅色的部分為oldCh和vCh
我們將它們?nèi)〕鰜聿⒎謩e用s和e指針指向它們的頭child和尾child
現(xiàn)在分別對(duì)oldS楚昭、oldE栖袋、S、E兩兩做sameVnode比較抚太,有四種比較方式塘幅,當(dāng)其中兩個(gè)能匹配上那么真實(shí)dom中的相應(yīng)節(jié)點(diǎn)會(huì)移到Vnode相應(yīng)的位置,這句話有點(diǎn)繞尿贫,打個(gè)比方
如果是oldS和E匹配上了电媳,那么真實(shí)dom中的第一個(gè)節(jié)點(diǎn)會(huì)移到最后
如果是oldE和S匹配上了,那么真實(shí)dom中的最后一個(gè)節(jié)點(diǎn)會(huì)移到最前庆亡,匹配上的兩個(gè)指針向中間移動(dòng)
如果四種匹配沒有一對(duì)是成功的匾乓,那么遍歷oldChild,S挨個(gè)和他們匹配又谋,匹配成功就在真實(shí)dom中將成功的節(jié)點(diǎn)移到最前面拼缝,如果依舊沒有成功的,那么將S對(duì)應(yīng)的節(jié)點(diǎn)插入到dom中對(duì)應(yīng)的oldS位置彰亥,oldS和S指針向中間移動(dòng)咧七。
再配個(gè)圖
第一步
oldS = a, oldE = d;
S = a, E = b;
oldS和S匹配任斋,則將dom中的a節(jié)點(diǎn)放到第一個(gè)继阻,已經(jīng)是第一個(gè)了就不管了,此時(shí)dom的位置為:a b d
第二步
oldS = b, oldE = d废酷;
S = c, E = b;
oldS和E匹配穴翩,就將原本的b節(jié)點(diǎn)移動(dòng)到最后,因?yàn)镋是最后一個(gè)節(jié)點(diǎn)锦积,他們位置要一致芒帕,這就是上面說的:當(dāng)其中兩個(gè)能匹配上那么真實(shí)dom中的相應(yīng)節(jié)點(diǎn)會(huì)移到Vnode相應(yīng)的位置,此時(shí)dom的位置為:a d b
第三步
oldS = d, oldE = d丰介;
S = c, E = d;
oldE和E匹配背蟆,位置不變此時(shí)dom的位置為:a d b
第四步
oldS++;
oldE--;
oldS > oldE;
遍歷結(jié)束鉴分,說明oldCh先遍歷完。就將剩余的vCh節(jié)點(diǎn)根據(jù)自己的的index插入到真實(shí)dom中去带膀,此時(shí)dom位置為:a c d b
一次模擬完成志珍。
這個(gè)匹配過程的結(jié)束有兩個(gè)條件:
- oldS > oldE表示oldCh先遍歷完,那么就將多余的vCh根據(jù)index添加到dom中去(如上圖)
-
S > E表示vCh先遍歷完垛叨,那么就在真實(shí)dom中將區(qū)間為[oldS, oldE]的多余節(jié)點(diǎn)刪掉
如下圖伦糯,新dom比舊dom少,最終為:a b e
patchnode3.png
再來一個(gè)例子嗽元,最終dom順序?yàn)椋篴 e b f
當(dāng)這些節(jié)點(diǎn)sameVnode成功后就會(huì)緊接著執(zhí)行patchVnode了敛纲,可以看一下上面的代碼
if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
}
就這樣層層遞歸下去,直到將oldVnode和Vnode中的所有子節(jié)點(diǎn)比對(duì)完剂癌。也將dom的所有補(bǔ)丁都打好啦淤翔。那么現(xiàn)在再回過去看updateChildren的代碼會(huì)不會(huì)容易很多呢?
參考:
https://www.cnblogs.com/wind-lanyan/p/9061684.html
https://www.cnblogs.com/isLiu/p/7909889.html
https://blog.csdn.net/m6i37jk/article/details/78140159