虚拟DOM和Diff算法
1. 什么是虚拟DOM和Diff算法
-
真实DOM:
<div class="container"> <h1>这是一个标签</h1> <ul> <li>vue</li> <li>react</li> <li>webpack</li> </ul> </div> -
虚拟DOM:
{ "sel": "div", "data": { "class": { "container": true } }, "childern": [ { "sel": "h1", "data": {}, "text": "这是一个标签" }, { "sel": "ul", "data": {}, "children": [ { "sel": "li", "data": {}, "text": "vue" }, { "sel": "li", "data": {}, "text": "react" }, { "sel": "li", "data": {}, "text": "webpack" } ] } ] } -
虚拟DOM是用JavaScript对象来描述一个DOM的层次结构,DOM中的一切属性在虚拟DOM中都有相关对应。
-
Diff算法实现的是最小量更新虚拟DOM。
-
注意:Diff实现的最小量更新仅发生在虚拟DOM上。
2. 认识Diff算法
Diff算法是对虚拟节点进行对比,通过patch函数来对比两个虚拟DON节点,从而实现最小量更新。
patch函数
import createElement from './createElement'
import patchVNode from './patchVNode'
import VNode from './vnode'
//patch用于对新旧DOM进行比较
export default function ( oldVNode, newVNode ) {
//如果旧的DOM是一个真实DOM,那么我们把他转化为虚拟DOM(VNode)
if ( oldVNode.sel === '' || oldVNode.sel === undefined ) {
oldVNode = VNode ( oldVNode.tagName.toLowerCase (), {}, [], undefined, oldVNode )
}
//如果newVNode和oldVNode是同一个DOM,那么我们对其进行精细化比较计算出最小更新量
if ( oldVNode.key === newVNode.key && oldVNode.sel === newVNode.sel ) {
patchVNode ( oldVNode, newVNode )
} else {
//如果newVNode和oldVNode是不同的DOM,那么我们暴力拆除oldVNode,添加newVNode
//构造新DOM的elm属性(elm是虚拟DOM对应的真实DOM)
let newVNodeElm = createElement ( newVNode )
//将newVNodeElm添加到oldVNode.elm之前
oldVNode.elm.parentNode.insertBefore ( newVNodeElm, oldVNode.elm )
//删除oldVNode.elm
oldVNode.elm.parentNode.removeChild ( oldVNode.elm )
}
}
patchVNode函数
import createElement from './createElement'
import updateChildren from './updateChildren'
export default function patchVNode ( oldVNode, newVNode ) {
//如果oldVNode === newVNode则退出函数
if ( oldVNode === newVNode ) return;
//如果newVNode有text属性并且newVNode没有children属性
if ( newVNode.text !== undefined && ( newVNode.children === undefined || newVNode.children.length === 0 ) ) {
//如果newVNode的text属性不等于oldVNode的text属性,则更改text的值
if ( newVNode.text !== oldVNode.text ) {
oldVNode.elm.innerText = newVNode.text
}
} else {
//如果newVNode有children属性,且oldVNode也有children属性,那么需要执行更新子节点策略updateChildren
if ( oldVNode.children !== undefined && oldVNode.children.length > 0 ) {
updateChildren ( oldVNode.elm, oldVNode.children, newVNode.children )
} else {
//如果newVNode有children属性,但是oldVNode没有children属性
//清空oldVNode.elm的text属性
oldVNode.elm.innerHTML = ''
//创建newVNode.children的真实DOM,并添加到oldVNode.elm中
for ( let i = 0; i < newVNode.children.length; i++ ) {
let childrenVNodeElm = createElement ( newVNode.children[i] )
oldVNode.elm.appendChild ( childrenVNodeElm )
}
}
}
//函数结束时,将老节点的elm属性赋值给新节点,保持elm属性的一致性
newVNode.elm = oldVNode.elm
}
createElement函数
//createElement用于创建一颗真实DOM树
export default function createElement ( VNode ) {
//创建dom节点
let domNode = document.createElement ( VNode.sel )
//如果虚拟dom没有子节点只有文本,我们直接向节点中添加文字,同时中止递归
if ( VNode.text !== '' && ( VNode.children === undefined || VNode.length === 0 ) ) {
//添加文本
domNode.innerText = VNode.text
} else if ( Array.isArray ( VNode.children ) && VNode.children.length > 0 ) {
//递归子节点
for ( let i = 0; i < VNode.children.length; i++ ) {
let ch = VNode.children[i]
//创建子节点dom,开始递归
let chDom = createElement ( ch )
//向dom节点中添加子节点
domNode.appendChild ( chDom )
}
}
//向虚拟DOM中追加elm属性
VNode.elm = domNode
return VNode.elm
}
updateChildren函数
import createElement from './createElement'
import patchVNode from './patchVNode'
function checkSameVNode ( a, b ) {
return a.sel === b.sel && a.key === b.key
}
export default function updateChildren ( parentElm, oldCh, newCh ) {
//旧前指针
let oldStartIdx = 0
//新前指针
let newStartIdx = 0
//旧后指针
let oldEndIdx = oldCh.length - 1
//新后指针
let newEndIdx = newCh.length - 1
//旧前节点
let oldStartVNode = oldCh[oldStartIdx]
//新前节点
let newStartVNode = newCh[newStartIdx]
//旧后节点
let oldEndVNode = oldCh[oldEndIdx]
//新后节点
let newEndVNode = newCh[newEndIdx]
//开始while
while ( oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx ) {
if ( !oldStartVNode ) {
oldStartVNode = [ ++oldStartIdx ]
} else if ( !oldEndVNode ) {
oldEndVNode = [ --oldEndIdx ]
} else if ( !newStartVNode ) {
newStartVNode = [ ++newStartIdx ]
} else if ( !newEndVNode ) {
newEndVNode = [ --newEndIdx ]
} else if ( checkSameVNode ( newStartVNode, oldStartVNode ) ) {
//1.新前和旧前命中
//对比新旧节点
patchVNode ( oldStartVNode, newStartVNode )
//指针移动
oldStartVNode = oldCh[++oldStartIdx]
newStartVNode = newCh[++newStartIdx]
} else if ( checkSameVNode ( newEndVNode, oldEndVNode ) ) {
//2新后和旧后命中
patchVNode ( oldEndVNode, newEndVNode )
oldEndVNode = oldCh[--oldEndIdx]
newEndVNode = newCh[--newEndIdx]
} else if ( checkSameVNode ( newEndVNode, oldStartVNode ) ) {
//3新后和旧前命中
patchVNode ( oldStartVNode, newEndVNode )
//将旧前指针指向的DOM移动到旧后指针指向的DOM的下一个DOM之前
parentElm.insertBefore ( oldStartVNode.elm, oldEndVNode.elm.nextSibling )
oldStartVNode = oldCh[++oldStartIdx]
newEndVNode = newCh[--newEndIdx]
} else if ( checkSameVNode ( newStartVNode, oldEndVNode ) ) {
//4新前和旧后命中
patchVNode ( oldEndVNode, newStartVNode )
//将旧后指针指向的DOM移动到旧前指针指向的DOM之前
parentElm.insertBefore ( oldEndVNode.elm, oldStartVNode.elm )
oldEndVNode = oldCh[--oldEndIdx]
newStartVNode = newCh[++newStartIdx]
} else {
//四种命中都没有命中,遍历寻找
//将旧的虚拟DOM的key作为键,列表的下标作为值
const keyMap = new Map ()
for ( let i = oldStartIdx; i <= oldEndIdx; i++ ) {
if ( oldCh[i] ) {
keyMap.set ( oldCh[i].key, i )
}
}
//查找旧节点中是否有新前指针指向的节点
const idxInOld = keyMap.get ( newStartVNode.key )
if ( !idxInOld ) {
//若没有,则在旧前指针指向的DOM前添加新DOM
parentElm.insertBefore ( createElement ( newStartVNode ), oldStartVNode.elm )
} else {
//若有,则patch两个DOM,将找到的DOM移动到旧前指针指向的DOM之前,将原位置赋值为undefined
const elmToMove = oldCh[idxInOld]
patchVNode ( elmToMove, newStartVNode )
oldCh[idxInOld] = undefined
parentElm.insertBefore ( elmToMove.elm, oldStartVNode.elm )
}
//遍历结束,移动新前指针
newStartVNode = newCh[++newStartIdx]
}
}
//如果遍历结束后,newStartIdx <= newEndIdx,说明新节点列表的长度大于旧节点列表,需要在旧节点列表中新增
if ( newStartIdx <= newEndIdx ) {
const before = newCh[newEndIdx + 1] ? newCh[newEndIdx + 1].elm : null
console.log ( before )
for ( let i = newStartIdx; i <= newEndIdx; i++ ) {
parentElm.insertBefore ( createElement ( newCh[i] ), before )
}
} else if ( oldStartIdx <= oldEndIdx ) {
//如果遍历结束后,oldStartIdx <= oldEndIdx,说明新节点列表的长度小于旧节点列表,需要在旧节点列表中删除
for ( let i = oldStartIdx; i <= oldEndIdx; i++ ) {
if ( oldCh[i] )
parentElm.removeChild ( oldCh[i].elm )
}
}
}