網(wǎng)上找的圖,懶得自己畫,畢竟本人PS一般(程序員程度的一般,對比設(shè)計師為未畢業(yè)渣渣級)

在這里也多說一句,節(jié)點對比不屬于diff算法,diff算法僅對于父節(jié)點一致,并且都有子節(jié)點的時候才需要用到,其他的就是簡單的邏輯判斷一直去if...else if...而已
開發(fā)準備工作,創(chuàng)建兩個vnode,前面方法已經(jīng)實現(xiàn)了,直接調(diào)用即可(開發(fā)完刪除即可)。
// index.js
import { compileToFunctions } from './compiler/index.js'
import { createdElm, patch } from './vdom/patch.js'
// 構(gòu)建兩個虛擬Dom
let vm1 = new Vue({
data () {
return {
name: 'yose'
}
}
})
let render1 = compileToFunctions(`<div id="a" a="1" style="color: red">{{name}}</div>`) // 將模板變?yōu)閞ender函數(shù)
let oldVnode = render1.call(vm1) // 老的虛擬節(jié)點
let el = createdElm(oldVnode) // 創(chuàng)建真實節(jié)點
document.body.appendChild(el)
let vm2 = new Vue({
data () {
return {
name: 'Catherine'
}
}
})
let render2 = compileToFunctions(`<div id="b" b="1" style="background: blue">{{name}}</div>`) // 將模板變?yōu)閞ender函數(shù)
let newVnode = render2.call(vm2) // 老的虛擬節(jié)點
setTimeout(() => {
patch(oldVnode, newVnode)
}, 2000)
父元素對比
patch負責(zé)兩件事:渲染成真實Dom(初渲染)及diff算法,之前diff已經(jīng)預(yù)留過,非真實節(jié)點!isRealElement走diff算法,現(xiàn)在就是來完善diff
父元素對比情況列舉:
- 對比父節(jié)點標簽,如果不一致,直接替換
- 標簽一致,但是為文本標簽(
tag為undefined),替換文本內(nèi)容 - 標簽一致,非文本標簽,但是屬性不同,復(fù)用老節(jié)點并且更新屬性
// vdom/patch.js
export function patch(oldVnode, vnode) {
if (isRealElement) {
// code...
} else {
+ // 1. 如果兩個虛擬節(jié)點的標簽不一致,就直接替換掉
+ if (oldVnode.tag !== vnode.tag) {
+ return oldVnode.el.parentNode.replaceChild(createdElm(vnode), oldVnode.el)
+ }
+ // 2. 標簽一樣,但是是兩個文本元素(tag: undefined)
+ if (!oldVnode.tag) {
+ if (oldVnode.text !== vnode.text) {
+ return oldVnode.el.textContent = vnode.text
+ }
+ }
+ // 3. 元素相同,屬性不同,復(fù)用老節(jié)點并且更新屬性
+ let el = vnode.el = oldVnode.el
+ // 用老的屬性和新的虛擬節(jié)點進行比對
+ updateProperties(vnode, oldVnode.data)
+ // 4. 更新子元素
+ let oldChildren = oldVnode.children || []
+ let newChildren = vnode.children || []
+ if (oldChildren.length > 0 && newChildren.length > 0) { // 新的老的都有子元素,需要使用diff算法
+ } else if (oldChildren.length > 0) { // 1. 老的有子元素,新的沒有子元素,刪除老的子元素
+ el.innerHTML = '' // 清空所有子節(jié)點
+ } else if (newChildren.length > 0) { // 2. 新的有子元素,老的沒有子元素,在老節(jié)點增加子元素即可
+ newChildren.forEach(child => el.appendChild(createElm(child)))
+ }
}
}
// 更新屬性,注意這里class與style無法處理表達式,因為從前面解析的時候就沒處理,還是那句,重點不在完全實現(xiàn),而是學(xué)習(xí)核心思路
function updateProperties (vnode, oldProps = {}) {
const newProps = vnode.data || {}
const el = vnode.el
+ // 1. 老的屬性,新的沒有,刪除屬性
+ // 前面提到過一次,以前vue1需要考慮重繪,現(xiàn)在新版瀏覽器已經(jīng)會做合并,所以不用再去考慮使用documentFlagment來優(yōu)化了
+ for (let key in oldProps) {
+ if (!newProps[key]) {
+ el.removeAttribute(key)
+ }
+ }
+ let newStyle = newProps.style || {}
+ let oldStyle = oldProps.style || {}
+ for (let key in oldStyle) { // 新老樣式先進行比對,刪除新vnode中沒有的樣式
+ if (!newStyle[key]) {
+ el.style[key] = ''
+ }
+ }
// 2. 新的屬性,老的沒有,直接用新的覆蓋,不用考慮有沒有
// 原本code...
}
diff算法
diff算法是當(dāng)父元素一致,并且都有子節(jié)點的情況下使用的
diff算法是借鑒于snabbdom.js的,有興趣的可自行拓展了解
diff算法的核心思路是去操作vnode,通過vnode來排查是否需要重新創(chuàng)建節(jié)點,而不是直接去訪問真實節(jié)點(減少過橋費)
對到節(jié)點,能移動的采用移動,能復(fù)用的節(jié)點則復(fù)用,不能移動或復(fù)用的才創(chuàng)建插入(減少節(jié)點的銷毀創(chuàng)建,因為Dom操作是具備移動性的,會移動節(jié)點,Dom映射)
還要注意一點,對比使用的vnode,移動真實Dom(這句在下面代碼最后一個情景里自行體會,比如后面比對可復(fù)用,會將節(jié)點置為null,置null的是虛擬節(jié)點,真實節(jié)點是直接移動)
// vdom\index.js
// 是否為相同虛擬節(jié)點
+ export function isSameVnode (oldVnode, newVnode) {
+ return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key)
+ }
diff算法主要實現(xiàn)邏輯代碼
export function patch(oldVnode, vnode) {
// code...
if (isRealElement) {
// code...
+ } else {
+ // 1. 如果兩個虛擬節(jié)點的標簽不一致,就直接替換掉
+ if (oldVnode.tag !== vnode.tag) {
+ return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el)
+ }
+
+ // 2. 標簽一樣,但是是兩個文本元素(tag: undefined)
+ if (!oldVnode.tag) {
+ if (oldVnode.text !== vnode.text) {
+ return oldVnode.el.textContent = vnode.text
+ }
+ }
+
+ // 3. 元素相同,屬性不同,復(fù)用老節(jié)點并且更新屬性
+ let el = vnode.el = oldVnode.el
+ // 用老的屬性和新的虛擬節(jié)點進行比對
+ updateProperties(vnode, oldVnode.data)
+
+ // 4. 更新子元素
+ let oldChildren = oldVnode.children || []
+ let newChildren = vnode.children || []
+
+ if (oldChildren.length > 0 && newChildren.length > 0) { // 新的老的都有子元素,需要使用diff算法
+ updateChildren(el, oldChildren, newChildren)
+ } else if (oldChildren.length > 0) { // 1. 老的有子元素,新的沒有子元素,刪除老的子元素
+ el.innerHTML = '' // 清空所有子節(jié)點
+ } else if (newChildren.length > 0) { // 2. 新的有子元素,老的沒有子元素,在老節(jié)點增加子元素即可
+ newChildren.forEach(child => el.appendChild(createElm(child)))
+ }
+ }
}
+ // diff算法主要邏輯
+ function updateChildren (parent, oldChildren, newChildren) {
+ let oldStartIndex = 0 // 老的父元素起始指針
+ let oldEndIndex = oldChildren.length - 1 // 老的父元素終止指針
+ let oldStartVnode = oldChildren[0] // 老的開始節(jié)點
+ let oldEndVnode = oldChildren[oldEndIndex] // 老的結(jié)束節(jié)點
+
+ let newStartIndex = 0 // 新的父元素起始指針
+ let newEndIndex = newChildren.length - 1 // 新的父元素終止指針
+ let newStartVnode = newChildren[0] // 新的開始節(jié)點
+ let newEndVnode = newChildren[newEndIndex] // 新的結(jié)束節(jié)點
+
+ // 創(chuàng)建字典表,用于亂序
+ function makeIndexByKey (oldChildren) {
+ let map = {}
+ oldChildren.forEach((item, index) => {
+ map[item.key] = index
+ })
+ return map
+ }
+
+ let map = makeIndexByKey(oldChildren)
+
+ // 1. 前端中比較常見的操作有:尾部插入 頭部插入 頭部移動到尾部 尾部移動到頭部 正序和反序
+ while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
+ if (!oldStartVnode) { // 亂序diff算法中處理過的虛擬節(jié)點
+ oldStartVnode = oldChildren[++oldStartIndex]
+ } else if (!oldEndVnode) { // 亂序diff算法中處理過的虛擬節(jié)點
+ oldEndVnode = oldChildren[--oldEndIndex]
+ } else if (isSameVnode(oldStartVnode, newStartVnode)) { // 向后插入操作,開始的虛擬節(jié)點一致
+ patch(oldStartVnode, newStartVnode) // 遞歸比對節(jié)點
+ oldStartVnode = oldChildren[++oldStartIndex]
+ newStartVnode = newChildren[++newStartIndex]
+ } else if (isSameVnode(oldEndVnode, newEndVnode)) { // 向前插入,開始的虛擬節(jié)點不一致,結(jié)束的虛擬節(jié)點一致
+ patch(oldEndVnode, newEndVnode)
+ oldEndVnode = oldChildren[--oldEndIndex]
+ newEndVnode = newChildren[--newEndIndex]
+ } else if (isSameVnode(oldStartVnode, newEndVnode)) { // 開始結(jié)束都不一致,舊的開始與新的結(jié)尾一致(頭部插入尾部)
+ patch(oldStartVnode, newEndVnode)
+ parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling)
+ oldStartVnode = oldChildren[++oldStartIndex]
+ newEndVnode = newChildren[--newEndIndex]
+ } else if (isSameVnode(oldEndVnode, newStartVnode)) { // 開始結(jié)束都不一致,舊的結(jié)尾與新的起始一致(尾部插入頭部)
+ patch(oldEndVnode, newStartVnode)
+ parent.insertBefore(oldEndVnode.el, oldStartVnode.el)
+ oldEndVnode = oldChildren[--oldEndIndex]
+ newStartVnode = newChildren[++newStartIndex]
+ } else { // 亂序diff算法,檢測是否有可復(fù)用的key值,有則將原本節(jié)點移動,老的位置置為null,否則將新的節(jié)點插入進老的節(jié)點中來
+ // 1. 需要先查找當(dāng)前索引 老節(jié)點索引和key的關(guān)系
+ // 移動的時候通過新的 key 去找對應(yīng)的老節(jié)點索引 => 獲取老節(jié)點,可以移動老節(jié)點
+ let moveIndex = map[newStartVnode.key]
+ if (moveIndex === undefined) { // 不在字典中存在,是個新節(jié)點,直接插入
+ parent.insertBefore(createElm(newStartVnode), oldStartVnode.el)
+ } else {
+ let moveVnode = oldChildren[moveIndex]
+ oldChildren[moveIndex] = undefined // 表示該虛擬節(jié)點已經(jīng)處理過,后續(xù)遞歸時可直接跳過
+ patch(moveVnode, newStartVnode) // 如果找到了,需要兩個虛擬節(jié)點對比
+ parent.insertBefore(moveVnode.el, oldStartVnode.el)
+ }
+ newStartVnode = newChildren[++newStartIndex]
+ }
+ }
+
+ // 新的比老的多,插入新節(jié)點
+ if (newStartIndex <= newEndIndex) {
+ // 將多出來的節(jié)點一個個插入進去
+ for (let i = newStartIndex; i <= newEndIndex; i++) {
+ // 排查下一個節(jié)點是否存在,如果存在證明指針是從后往前(insertBefore),反之指針是從頭往后(appendChild)
+ let nextEle = newChildren[newEndIndex + 1] === undefined ? null : newChildren[newEndIndex + 1].el
+ // 這里不需要分情況使用 appendChild 或 insertBefore
+ // 如果 insertBefore 傳入 null,等價于 appendChild
+ parent.insertBefore(createElm(newChildren[i]), nextEle)
+ }
+ }
+
+ // 老的比新的多,刪除老節(jié)點
+ if (oldStartIndex <= oldEndIndex) {
+ for (let i = oldStartIndex; i <= oldEndIndex; i++) {
+ let child = oldChildren[i]
+ if (child !== undefined) { // 有可能是遍歷到已經(jīng)被使用過的虛擬節(jié)點,需要排除掉
+ parent.removeChild(child.el)
+ }
+ }
+ }
+ }
// 更新屬性,注意這里class與style無法處理表達式,因為從前面解析的時候就沒處理,還是那句,重點不在完全實現(xiàn),而是學(xué)習(xí)核心思路
function updateProperties (vnode, oldProps = {}) {
const newProps = vnode.data || {}
const el = vnode.el
+ // 1. 老的屬性,新的沒有,刪除屬性
+ // 前面提到過一次,以前vue1需要考慮重繪,現(xiàn)在新版瀏覽器已經(jīng)會做合并,所以不用再去考慮使用documentFlagment來優(yōu)化了
+ for (let key in oldProps) {
+ if (!newProps[key]) {
+ el.removeAttribute(key)
+ }
+ }
+ let newStyle = newProps.style || {}
+ let oldStyle = oldProps.style || {}
+ for (let key in oldStyle) { // 新老樣式先進行比對,刪除新vnode中沒有的樣式
+ if (!newStyle[key]) {
+ el.style[key] = ''
+ }
+ }
+ // 2. 新的屬性,老的沒有,直接用新的覆蓋,不用考慮有沒有
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName]
}
} else if (typeof tag === 'class') { // 靜態(tài)的class可以沒有這段,但還是寫上,假裝如果是class可以處理簡單的表達式
vnode.className = newProps.class
} else {
el.setAttribute(key, newProps[key])
}
}
}
vue更新時引入diff算法
diff算法是在渲染更新時才需要使用的,前面實現(xiàn)時,_update使用了一個虛擬節(jié)點,所以現(xiàn)在要在實例上再掛一個_vnode,用于保存上一次的虛擬節(jié)點(patch需要老虛擬節(jié)點與新虛擬節(jié)點做對比)
// lifecycle.js
export function lifecycleMixin (Vue) {
// 視圖更新方法,用于渲染真實DOM
Vue.prototype._update = function (vnode) {
const vm = this
+ const preVnode = vm._vnode // 初始化時必然為undefind
+ vm._vnode = vnode
+
+ if (!preVnode) { // 初渲染
+ // 首次渲染,需要用虛擬節(jié)點,來更新真實的dom元素(vm._render())
+ // 第一次渲染完畢后 拿到新的節(jié)點,下次再次渲染時替換上次渲染的結(jié)果
vm.$el = patch(vm.$el, vnode) // 組件調(diào)用patch方法后會產(chǎn)生$el屬性
+ } else { // 視圖更新渲染
+ vm.$el = patch(preVnode, vnode)
+ }
}
}