Vue中虛擬DOM的簡單實現(xiàn)
虛擬DOM
1. 什么是虛擬DOM?為什么要有虛擬DOM?
- 虛擬DOM 是用于描述 DOM 節(jié)點的 JS 對象。
- 操作真實DOM非常 耗費性能 。盡可能 減少 對 DOM 的操作,通過犧牲 JS 的計算性能來換取操作 DOM 所消耗的性能。
2. VNode類
省略了部分屬性
/** src/core/vdom/vnode.ts **/ export default class VNode { ? ? tag; ? ? ? ? ? ? ? ? // 當(dāng)前節(jié)點的標(biāo)簽名 ? ? data; ? ? ? ? ? ? ? ?// 當(dāng)前節(jié)點對應(yīng)的對象,包含了一些具體數(shù)據(jù)信息 ? ? children; ? ? ? ? ? ?// 當(dāng)前節(jié)點的子節(jié)點數(shù)組 ? ? text; ? ? ? ? ? ? ? ?// 當(dāng)前節(jié)點的文本 ? ? elm; ? ? ? ? ? ? ? ? // 當(dāng)前節(jié)點對應(yīng)的真實DOM節(jié)點 ? ? context; ? ? ? ? ? ? // 當(dāng)前節(jié)點的上下文(Vue實例) ? ? componentInstance; ? // 當(dāng)前節(jié)點對應(yīng)的組件實例 ? ? parent; ? ? ? ? ? ? ?// 當(dāng)前節(jié)點對應(yīng)的真實的父DOM節(jié)點 ? ? // diff 優(yōu)化的屬性 ? ? isStatic; ? ? ? ? ? ?// 是否靜態(tài)節(jié)點,是則跳過 diff ? ? constructor( ? ? ? ? tag?, ? ? ? ? data?, ? ? ? ? children?, ? ? ? ? text?, ? ? ? ? elm?, ? ? ? ? context? ? ? ) { ? ? ? ? this.tag = tag; ? ? ? ? this.data = data; ? ? ? ? this.children = children; ? ? ? ? this.text = text; ? ? ? ? this.elm = elm; ? ? ? ? this.context = context; ? ? ? ? this.componentInstance = undefined; ? ? ? ? this.parent = undefined; ? ? ? ? this.isStatic = false; ? ? } }
3. VNode的類型
3.1 注釋節(jié)點
注釋節(jié)點的描述非常簡單: vnode.text 表示注釋內(nèi)容, vnode.isComment 表示是一個注釋節(jié)點。
/** src/core/vdom/vnode.ts **/ export const createEmptyVNode = (text) => { ? ? const node = new VNode(); ? ? node.text = text; ? ? node.isComment = true; ? ? return node; };
3.2 文本節(jié)點
文本節(jié)點 的描述比 注釋節(jié)點 更簡單,只需要一個 text 屬性。
/** src/core/vdom/vnode.ts **/ export function createTextVNode(val) { ? ? return VNode(undefined, undefined, undefined, String(val)); };
3.3 克隆節(jié)點
克隆節(jié)點是復(fù)制一個已存在的節(jié)點,主要是為了做 模版編譯優(yōu)化 時使用。
克隆時會新建一個 VNode實例 ,然后將需要復(fù)制的節(jié)點信息 淺拷貝 到新的節(jié)點上,并通過 vnode.isCloned 標(biāo)識該節(jié)點是克隆節(jié)點。
/** src/core/vdom/vnode.ts **/ export function cloneVNode(vnode: VNode): VNode { ? const cloned = new VNode( ? ? vnode.tag, ? ? vnode.data, ? ? vnode.children && vnode.children.slice(), ? ? vnode.text, ? ? vnode.elm, ? ? vnode.context, ? ? vnode.componentOptions, ? ? vnode.asyncFactory ? ); ? cloned.ns = vnode.ns; ? cloned.isStatic = vnode.isStatic; ? cloned.key = vnode.key; ? cloned.isComment = vnode.isComment; ? cloned.fnContext = vnode.fnContext; ? cloned.fnOptions = vnode.fnOptions; ? cloned.fnScopeId = vnode.fnScopeId; ? cloned.asyncMeta = vnode.asyncMeta; ? cloned.isCloned = true; ? return cloned; };
3.4 元素節(jié)點
/** src/core/vdom/create-element.ts **/ export function _createElement( ? ? context, ? ? tag?, ? ? data?, ? ? children? ) { ? ? // 如果 data 存在且已轉(zhuǎn)成可觀測對象,則返回一個注釋節(jié)點 ? ? if (isDef(data) && isDef(data.__ob__)) { ? ? ? ? return createEmptyVNode(); ? ? } ? ? // 根據(jù) data.is 重新給 tag 賦值 ? ? // :is 的實現(xiàn)原理 ? ? if (isDef(data) && isDef(data.is)) { ? ? ? ? tag = data.is; ? ? } ? ? // 如果不存在 tag ,則返回一個注釋節(jié)點 ? ? if (!tag) { ? ? ? ? return createEmptyVNode(); ? ? } ? ? let vnode; ? ? if (typeof tag === 'string') { ? ? ? ? let Ctor; ? ? ? ? if ((!data || !data.pre) && isDef((Ctor = resolveAsset(context.$options, 'components', tag)))) { ? ? ? ? ? ? // 根據(jù) tag 從 options.components 中獲取要創(chuàng)建的組件節(jié)點 ? ? ? ? ? ? vnode = createComponent(Ctor, data, context, children); ? ? ? ? } else { ? ? ? ? ? ? // 創(chuàng)建普通的 vnode 節(jié)點 ? ? ? ? ? ? vnode = new VNode(tag, data, children, undefined, undefined, context); ? ? ? ? } ? ? } else { ? ? ? ? // 創(chuàng)建組件節(jié)點 ? ? ? ? vnode = createComponent(tag, data, context, children); ? ? } ? ? return vnode; }
3.5 組件節(jié)點
組件節(jié)點除了元素節(jié)點具有的屬性外,還有兩個特有屬性:
- componentOptions: 組件的 option選項,如組件的 props 等
- componentInstance: 組件節(jié)點對應(yīng)的 Vue實例
3.6 函數(shù)式組件節(jié)點
函數(shù)式組件節(jié)點相較于組件節(jié)點又有兩個特有屬性:
- fnContext: 函數(shù)式組件對應(yīng)的 Vue實例
- fnOptions: 組件的 option選項
4. 總結(jié)
在視圖渲染之前,將寫好的 template模版 編譯成 vnode 緩存下來。等到 數(shù)據(jù)發(fā)生變化 頁面需要 重新渲染 時,將數(shù)據(jù)發(fā)生變化后生成的 vnode 與前一次緩存的 vnode 進行對比,找出差異,根據(jù) 有差異的vnode 創(chuàng)建 真實DOM節(jié)點再插入到視圖中,完成試圖更新。
Diff
1. 創(chuàng)建節(jié)點
為了避免直接修改 vnode 而引起 狀態(tài)混亂 問題,創(chuàng)建節(jié)點時若 vnode 已被之前的渲染使用,則 克隆該節(jié)點 ,修改克隆的 vnode 的屬性。
創(chuàng)建節(jié)點時,會根據(jù)當(dāng)前 宿主環(huán)境 調(diào)用封裝好的 nodeOps.createElement() 方法,在 web端 等同于 document.createElement() 。
/** src/core/vdom/patch.ts **/ function createElm( ? ? vnode, ? ? insertedVnodeQueue, ? ? parentElm, ? ? refElm, ? ? nested, ? ? ownerArray, ? ? index ) { ? ? // vnode 有真實DOM節(jié)點時,克隆生成新的 vnode ? ? if (isDef(vnode.elm) && isDef(ownerArray)) { ? ? ? ? vnode = ownerArray[index] = cloneVNode(vnode); ? ? } ? ? // 是否是組件的根節(jié)點 ? ? vnode.isRootInsert = !nested; ? ? const data = vnode.data; ? ? const children = vnode.children; ? ? const tag = vnode.tag; ? ? if (isDef(tag)) { ? ? ? ? // tag 不為 null/undefined 時 ? ? ? ? // 創(chuàng)建新的真實DOM節(jié)點 ? ? ? ? vnode.elm = nodeOps.createElement(tag, vnode); ? ? ? ? // 創(chuàng)建子節(jié)點 ? ? ? ? createChildren(vnode, children, insertedVnodeQueue); ? ? ? ? // 插入節(jié)點 ? ? ? ? insert(parentElm, vnode.elm, refElm); ? ? } else if (isTrue(vnode.isComment)) { ? ? ? ? // 創(chuàng)建注釋節(jié)點 ? ? ? ? vnode.elm = nodeOps.createComment(vnode.text); ? ? ? ? insert(parentElm, vnode.elm, refElm); ? ? } else { ? ? ? ? // 創(chuàng)建文本節(jié)點 ? ? ? ? vnode.elm = nodeOps.createTextNode(vnode.text); ? ? ? ? insert(parentElm, vnode.elm, refElm); ? ? } }
2. 刪除節(jié)點
刪除節(jié)點的邏輯非常簡單,找到 父節(jié)點 再移除 子節(jié)點 。
/** src/core/vdom/patch.ts **/ function removeNode(el) { ? ? const parent = nodeOps.parentNode(el); ? ? nodeOps.removeChild(parent, el); }
3. 更新節(jié)點
更新節(jié)點比較復(fù)雜:
- 判斷新/舊節(jié)點是否相同,若 新/舊節(jié)點相同 , 則 結(jié)束更新流程
- 克隆節(jié)點
- 新舊節(jié)點是否為 靜態(tài)節(jié)點 ,新/舊節(jié)點的 key 是否相同,新節(jié)點是否為 克隆節(jié)點 或新節(jié)點是否只創(chuàng)建一次。若 新/舊節(jié)點都為靜態(tài)節(jié)點,新舊節(jié)點的 key相同 ,新節(jié)點為克隆節(jié)點或新節(jié)點只能被創(chuàng)建一次 ,則更新 vnode.componentInstance 并 結(jié)束更新流程
- 新節(jié)點是否包含文本
- 新節(jié)點不包含文本
- 新/舊節(jié)點都包含子節(jié)點,且子節(jié)點不同 ,則 更新子節(jié)點
- 只有新節(jié)點包含子節(jié)點 ,則 清空DOM中的文本內(nèi)容,并更新子節(jié)點
- 只有舊節(jié)點包含子節(jié)點 ,則 移除子節(jié)點
- 新/舊節(jié)點都不包含子節(jié)點,且舊節(jié)點包含文本 ,則 清空DOM中的文本內(nèi)容
- 新節(jié)點包含文本,且新/舊節(jié)點的文本不同 ,則 更新DOM中的文本內(nèi)容
/** src/core/vdom/patch.ts **/ function patchVnode( ? ? oldVnode, ? ? vnode, ? ? insertedVnodeQueue, ? ? ownerArray, ? ? index, ? ? removeOnly? ) { ? ? // 新、舊節(jié)點相同,直接返回 ? ? if (oldVnode === vnode) { ? ? ? ? return; ? ? } ? ? // 克隆節(jié)點,為什么需要克隆節(jié)點的原因不做贅述 ? ? if (isDef(vnode.elm) && isDef(ownerArray)) { ? ? ? ? vnode = ownerArray[index] = cloneVNode(vnode); ? ? } ? ? // 重新對克隆節(jié)點的真實DOM賦值 ? ? const elm = (vnode.elm = oldVnode.elm); ? ? // vnode 與 oldVnode 都是靜態(tài)節(jié)點,且 key 相同,直接返回 ? ? if ( ? ? ? ? isTrue(vnode.isStatic) && ? ? ? ? isTrue(oldVnode.isStatic) && ? ? ? ? vnode.key === oldVnode.key && ? ? ? ? (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ? ? ) { ? ? ? ? vnode.componentInstance = oldVnode.componentInstance; ? ? ? ? return; ? ? } ? ? const oldCh = oldVnode.children; ? ? const ch = vnode.children; ? ? // vnode 沒有文本屬性 ? ? if (isUndef(vnode.text)) { ? ? ? ? if (isDef(oldCh) && isDef(ch)) { ? ? ? ? ? ? // 若存在 oldCh 和 ch ,且二者不同 ? ? ? ? ? ? // 則更新子節(jié)點 ? ? ? ? ? ? if (oldCh !== ch) { ? ? ? ? ? ? ? ? updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); ? ? ? ? ? ? } ? ? ? ? } else if (isDef(ch)) { ? ? ? ? ? ? // 若只存在 ch? ? ? ? ? ? ? // 則清空 DOM 中的文本,再添加子節(jié)點 ? ? ? ? ? ? if (isDef(oldVnode.text)) { ? ? ? ? ? ? ? ? nodeOps.setTextContent(elm, ''); ? ? ? ? ? ? } ? ? ? ? ? ? addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); ? ? ? ? } else if (isDef(oldCh)) { ? ? ? ? ? ? // 若只存在 oldCh ? ? ? ? ? ? // 則移除子節(jié)點 ? ? ? ? ? ? removeVnodes(oldCh, 0, oldCh.length - 1); ? ? ? ? } else if (isDef(oldVnode.text)) { ? ? ? ? ? ? // 若都沒有子節(jié)點,且 oldVnode 有文本屬性 ? ? ? ? ? ? // 則清空 DOM 中的文本 ? ? ? ? ? ? nodeOps.setTextContent(elm, ''); ? ? ? ? } ? ? } else if (oldVnode.text !== vnode.text) { ? ? ? ? // vnode 有文本屬性則與 oldVnode 的文本屬性比較 ? ? ? ? // 若有差異則更新為新的文本 ? ? ? ? nodeOps.setTextContent(elm, vnode.text); ? ? } }
4. 總結(jié)
Vue 的 diff算法(patch) 的過程干了三件事: 創(chuàng)建節(jié)點,刪除節(jié)點,更新節(jié)點。
更新子節(jié)點
Vue 中通過 updateChildren()方法 更新子節(jié)點。其思想就是循環(huán)新/舊子節(jié)點,然后對比。這部分代碼較長,按照代碼結(jié)構(gòu)逐步分析。
對源碼中的一些屬性的中文名作以下約定:
- 新子節(jié)點數(shù)組中第一個未處理的子節(jié)點: newStartVnode 新前
- 新子節(jié)點數(shù)組中最后一個未處理的子節(jié)點:newEndVnode 新后
- 舊子節(jié)點數(shù)組中第一個未處理的子節(jié)點: oldStartVnode 舊前
- 舊子節(jié)點數(shù)組中最后一個未處理的子節(jié)點:oldEndVnode 舊后
1. 新前與舊前相同
/** src/core/vdom/patch.ts **/ else if (sameVnode(oldStartVnode, newStartVnode)) { ? ?// 進入 patch 流程,對比新舊 vnode 更新節(jié)點 ? ?patchVnode( ? ? ? ?oldStartVnode, ? ? ? ?newStartVnode, ? ? ? ?insertedVnodeQueue, ? ? ? ?newCh, ? ? ? ?newStartIdx ? ?); ? ?// 從前向后切換待處理子節(jié)點 ? ?oldStartVnode = oldCh[++oldStartIdx]; ? ?newStartVnode = newCh[++newStartIdx]; }
2. 新后與舊后相同
/** src/core/vdom/patch.ts **/ else if (sameVnode(oldEndVnode, newEndVnode)) { ? ?// 進入 patch 流程,對比新舊 vnode 更新節(jié)點 ? ?patchVnode( ? ? ? ?oldEndVnode, ? ? ? ?newEndVnode, ? ? ? ?insertedVnodeQueue, ? ? ? ?newCh, ? ? ? ?newEndIdx ? ?); ? ?// 從后向前切換待處理子節(jié)點 ? ?oldEndVnode = oldCh[--oldEndIdx]; ? ?newEndVnode = newCh[--newEndIdx]; }
3. 新后與舊前相同
/** src/core/vdom/patch.ts **/ else if (sameVnode(oldStartVnode, newEndVnode)) { ? ? // 進入 patch 流程,更新節(jié)點 ? ? // 省略調(diào)用 patchVnode 的代碼 ? ? // ... ? ? // 將舊前節(jié)點插到舊后節(jié)點后面 ? ? nodeOps.insertBefore( ? ? ? ? parentElm, ? ? ? ? oldStartVnode.elm, ? ? ? ? nodeOps.nextSibling(oldEndVnode.elm) ? ? ); ? ? // 切換待處理子節(jié)點 ? ? oldStartVnode = oldCh[++oldStartIdx]; ? ? newEndVnode = newCh[--newEndIdx]; }
4. 新前與舊后相同
/** src/core/vdom/patch.ts **/ else if (sameVnode(oldEndVnode, newStartVnode)) { ? ? // 進入 patch 流程,更新節(jié)點 ? ? // 省略調(diào)用 patchVnode 的代碼 ? ? // ... ? ? // 將舊后節(jié)點插到舊前前面 ? ? nodeOps.insertBefore( ? ? ? ? parentElm, ? ? ? ? oldEndVnode.elm, ? ? ? ? oldStartVnode.elm ? ? ); ? ? // 切換待處理子節(jié)點 ? ? oldEndVnode = oldCh[--oldEndIdx]; ? ? newStartVnode = newCh[++newStartIdx]; }
5. 不滿足以上4種情況
/** src/core/vdom/patch.ts **/ else { ? ? // idxInOld 有兩種取值方法 ? ? // 1. 獲取 舊節(jié)點數(shù)組中 與 新節(jié)點的 key 相同的 vnode 的索引 ? ? // 2. 舊節(jié)點數(shù)組中 與 新節(jié)點相同的 vnode 的索引 ? ? if (isUndef(idxInOld)) { ? ? ? ? // 舊子節(jié)點數(shù)組 中不存在 新前 ? ? ? ? // 創(chuàng)建新元素 ? ? ? ? createElm( ? ? ? ? ? ? newStartVnode, ? ? ? ? ? ? insertedVnodeQueue, ? ? ? ? ? ? parentElm, ? ? ? ? ? ? oldStartVnode.elm, ? ? ? ? ? ? false, ? ? ? ? ? ? newCh, ? ? ? ? ? ? newStartIdx ? ? ? ? ); ? ? } else { ? ? ? ? vnodeToMove = oldCh[idxInOld]; ? ? ? ? if (sameVnode(vnodeToMove, newStartVnode)) { ? ? ? ? ? ? // 進入 patch 流程,更新節(jié)點 ? ? ? ? ? ? // 省略調(diào)用 patchVnode 的代碼 ? ? ? ? ? ? // ... ? ? ? ? ? ? oldCh[idxInOld] = undefined; ? ? ? ? ? ? // 將節(jié)點移動到 舊前節(jié)點 前面 ? ? ? ? ? ? nodeOps.insertBefore( ? ? ? ? ? ? ? ? parentElm, ? ? ? ? ? ? ? ? vnodeToMove.elm, ? ? ? ? ? ? ? ? oldStartVnode.elm ? ? ? ? ? ? ); ? ? ? ? } else { ? ? ? ? ? ? // vnode 不同,則創(chuàng)建新元素 ? ? ? ? ? ? createElm( ? ? ? ? ? ? ? ? newStartVnode, ? ? ? ? ? ? ? ? insertedVnodeQueue, ? ? ? ? ? ? ? ? parentElm, ? ? ? ? ? ? ? ? oldStartVnode.elm, ? ? ? ? ? ? ? ? false, ? ? ? ? ? ? ? ? newCh, ? ? ? ? ? ? ? ? newStartIdx ? ? ? ? ? ? ); ? ? ? ? } ? ? } }
6. 結(jié)束while循環(huán)中的邏輯后
if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm; // 插入新節(jié)點 addVnodes( parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue ); } else if (newStartIdx > newEndIdx) { // 移除舊子節(jié)點數(shù)組中剩余未處理的節(jié)點 removeNodes(oldCh, oldStartIdx, oldEndIdx); }
到此這篇關(guān)于Vue中虛擬DOM的簡單實現(xiàn)的文章就介紹到這了,更多相關(guān)Vue 虛擬DOM內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue項目實現(xiàn)會議預(yù)約功能(包含某天的某個時間段和某月的某幾天)
這篇文章主要介紹了vue項目實現(xiàn)會議預(yù)約功能(包含某天的某個時間段和某月的某幾天),本文結(jié)合實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-02-02Vuejs入門教程之Vue生命周期,數(shù)據(jù),手動掛載,指令,過濾器
本篇文章主要介紹了Vuejs入門教程之Vue生命周期,數(shù)據(jù),手動掛載,指令,過濾器的相關(guān)知識。具有很好的參考價值。下面跟著小編一起來看下吧2017-04-04