前言
Vue 源码中虚构 DOM 与 Diff 算法的实现借鉴了 snabbdom
这个库,snabbdom
是一个虚构 DOM 库,它专一于简略,模块化,弱小的性能和性能。要彻底明确虚构 DOM 与 Diff 算法就得剖析 snabbdom
这个库到底做了什么?
获取源代码
能够通过npm i snabbdom -D
来下载 snabbdom
库,这样咱们既能看到 src
下用 Typescript 编写的源码,也能看到编译好的 JavaScript 代码。上面贴的源码是 2.1.0
版本,当初曾经更新到 3.0.3
版本了。倡议将下方呈现的源码复制到下载的 snabbdom
库中相应地位,这样看源码比拟清晰。那咱们就开始剖析源码吧。
源码剖析
JavaScript 对象模仿实在 DOM 树
通过调用 snabbdom
库中的 h
函数就能够对实在 DOM 节点进行形象。咱们先来看看一个残缺的虚构 DOM 节点(vnode)是什么样的:
{
sel: "div", // 以后 vnode 的选择器
elm: undefined, // 以后 vnode 对应实在的 DOM 节点
key: undefined, // 以后 vnode 的惟一标识
data: {}, // 以后 vnode 的属性,款式等
children: undefined, // 以后 vnode 的子元素
text: '文本内容' // 以后 vnode 的文本节点内容
}
实际上,h
函数的作用就是用 JavaScript 对象模仿实在的 DOM 树,对实在 DOM 树进行形象。调用 h
函数就能失去由 vnode 组成的虚构 DOM 树。
调用 h
函数有多种形式:
① h('div')
② h('div', 'text')
③ h('div', h('p'))
④ h('div', [])
⑤ h('div', {})
⑥ h('div', {}, 'text')
⑦ h('div', {}, h('span'))
⑧ h('div', {}, [])
使得 h
函数的第二和第三个参数比拟灵便,要判断的状况也比拟多,上面把这部分的外围源码剖析贴一下:
// h 函数:依据传入的参数揣测出 h 函数的调用模式以及每个 vnode 对应属性的属性值
export function h(sel: string): VNode
export function h(sel: string, data: VNodeData | null): VNode
export function h(sel: string, children: VNodeChildren): VNode
export function h(sel: string, data: VNodeData | null, children: VNodeChildren): VNode
export function h(sel: any, b?: any, c?: any): VNode {var data: VNodeData = {};
var children: any;
var text: any;
var i: number
// c 有值,状况有:⑥ ⑦ ⑧
if (c !== undefined) {
// c 有值的状况下 b 有值,状况有:⑥ ⑦ ⑧
if (b !== null) {
// 将 b 赋值给 data
data = b
}
// c 的数据类型是数组,状况有:⑧
if (is.array(c)) {
children = c
// 判断 c 是文本节点,状况有:⑥
} else if (is.primitive(c)) {
text = c
// 状况有:⑦,⑦这条语句会先执行 h('span')代码,间接调用 vnode 函数,调用后会返回{sel: 'span'},// 这时 c 有值并且 c 并且含有 sel 属性
} else if (c && c.sel) {// 注:这里的 c 不是 h('span'),而是 h('span')的返回值,是个 {sel: 'span'} 这样的对象,// 最初组装成数组赋值给 children
children =
}
// c 没有值,b 有值,状况有:② ③ ④ ⑤
} else if (b !== undefined && b !== null) {
// b 的数据类型是数组,状况有:④
if (is.array(b)) {
children = b
// 判断 b 是文本节点,状况有:②
} else if (is.primitive(b)) {
text = b
// 状况有:③,③这条语句会先执行 h('p')代码,间接调用 vnode 函数,调用后会返回{sel: 'p'},// 这时 b 有值并且 b 并且含有 sel 属性
} else if (b && b.sel) {// 注:这里的 b 不是 h('p'),而是 h('p')的返回值,是个 {sel: 'p'} 这样的对象,// 最初组装成数组赋值给 children
children = [b]
// 状况有:⑤,将 b 赋值给 data
} else {data = b}
}
// children 有值,遍历 children
if (children !== undefined) {for (i = 0; i < children.length; ++i) {
// 判断 children 中的每一项的数据类型是否是 string/number,调用 vnode 函数
if (is.primitive(children[i])) {children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
}
}
}
/**
* 调用 vnode 后返回形如
* {
* sel: 'div',
* data: {style: '#000'},
* children: undefined,
* text: 'text',
* elm: undefined,
* key: undefined
* }
* 这样的 JavaScript 对象
*/
return vnode(sel, data, children, text, undefined);
}
// vnode 函数:依据传入的参数组装 vnode 构造
export function vnode(sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined): VNode {
// 判断 data 是否有值,有值就将 data.key 赋值给 key,无值就将 undefined 赋值给 key
const key = data === undefined ? undefined : data.key
// 将传入 vnode 函数的参数组装成一个对象返回
return {sel, data, children, text, elm, key}
}
diff 算法 – 入口函数
通过 h
函数失去新旧虚构节点 DOM 对象后就能够进行差别比拟了。在理论应用过程中,咱们是间接调用 snabbdom
的 patch
函数,而后传入两个参数,通过 patch
函数外部解决就能够失去新旧虚构节点 DOM 对象的差别,并将差别局部更新到真正的 DOM 树上。
首先,patch
函数会去判断 oldVnode
是否是实在 DOM 节点,如果是则须要先转换为虚构 DOM 节点 oldVnode = emptyNodeAt(oldVnode)
;而后去比拟新旧 vnode 是否是同一个节点 sameVnode(oldVnode, vnode)
,如果是同一节点则准确比拟新旧 vnode patchVnode(oldVnode, vnode, insertedVnodeQueue)
,如果不是则间接创立新 vnode 对应的实在 DOM 节点 createElm(vnode, insertedVnodeQueue)
,在 createElm 函数中创立新 vnode 的实在 DOM 节点以及它对应的子节点,并把子节点插入到相应地位。如果 oldVnode.elm 有父节点则把新 vnode 对应的实在 DOM 节点作为子节点插入到相应地位,并且删除旧节点。上面贴一下 patch
函数的源码解析:
function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node
const insertedVnodeQueue: VNodeQueue = []
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
// isVnode(oldVnode)判断 oldVnode.sel 是否存在,不存在示意 oldVnode 是实在的 DOM 节点
if (!isVnode(oldVnode)) {
// oldVnode 可能是实在的 DOM 节点,也可能是旧的虚构 DOM 节点,// 如果是实在的 DOM 节点要调用 vnode 函数组装成虚构 DOM 节点
oldVnode = emptyNodeAt(oldVnode)
}
// 判断出是同一个虚构 DOM 节点
if (sameVnode(oldVnode, vnode)) {
// 准确比拟两个虚构 DOM 节点
patchVnode(oldVnode, vnode, insertedVnodeQueue)
} else {
// oldVnode.elm 是虚构 DOM 节点对应的实在 DOM 节点
elm = oldVnode.elm!
// api.parentNode(elm)获取 elm 的父节点 elm.parentNode
parent = api.parentNode(elm) as Node
// 创立 vnode 下实在 DOM 节点并更新到相应地位
createElm(vnode, insertedVnodeQueue)
// elm 的父节点存在
if (parent !== null) {// api.nextSibling(elm)-->elm.nextSibling 返回紧跟在 elm 之后的节点
// api.insertBefore(parent, B, C)-->-->parent.insertBefore(B, C),将 B 节点插入到 C 节点之前
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
removeVnodes(parent, [oldVnode], 0, 0) // 删除旧的 DOM 节点
}
}
for (i = 0; i < insertedVnodeQueue.length; ++i) {insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
return vnode
}
patch
函数中用到了 emptyNodeAt
函数,这个函数次要是解决 patch
函数第一个参数为实在 DOM 节点的状况。上面贴一下这个函数的源码解析:
function emptyNodeAt(elm: Element) {
// 判断传入的 DOM 节点 elm 有没有 id 属性,因为虚构 DOM 节点的 sel 属性是选择器,例如:div#wrap
const id = elm.id ? '#' + elm.id : ''
// 判断传入的 ODM 节点 elm 有没有 class 属性,因为虚构 DOM 节点的 sel 属性是选择器,例如:div.wrap
const c = elm.className ? '.' + elm.className.split('').join('.') :''
// 调用 vnode 函数将传入的 DOM 节点组装成虚构 DOM 节点
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm)
}
patch
函数中用到了 sameVnode
函数,这个函数次要用来比拟两个虚构 DOM 节点是否是同一个虚构节点。上面贴一下这个函数的源码剖析:
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
// 判断 vnode1 和 vnode2 是否是同一个虚构 DOM 节点
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}
diff 算法 – 新旧 vnode 不是同一个节点的状况
依据 sameVnode
函数返回的后果,新旧 vnode 不是同一个虚构节点。首先获取到 oldVnode 对应实在 DOM 节点的父节点 parent
,而后调用 createElm
函数去创立 vnode 对应的实在 DOM 节点以及它的子节点和标签属性等等。判断是否有 parent
,如果有则将 vnode.elm 对应的 DOM 节点作为子节点插入到 parent
节点下的相应地位。局部源码剖析在 patch
函数中,上面贴一下 createElm
函数的源码剖析:
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any
let data = vnode.data
if (data !== undefined) {
const init = data.hook?.init
if (isDef(init)) {init(vnode)
data = vnode.data
}
}
const children = vnode.children
const sel = vnode.sel
// 判断 sel 值中是否蕴含!
if (sel === '!') {if (isUndef(vnode.text)) {vnode.text = ''}
// --> document.createComment(vnode.text!)创立正文节点
vnode.elm = api.createComment(vnode.text!)
} else if (sel !== undefined) {
// 解析 sel 选择器
// 查找 sel 属性值中 #的索引,没找到返回 -1
const hashIdx = sel.indexOf('#')
// hashIdx 作为起始地位查找 sel 属性值中. 的索引,如果 hashIdx < 0 那么从地位 0 开始查找
const dotIdx = sel.indexOf('.', hashIdx)
const hash = hashIdx > 0 ? hashIdx : sel.length
const dot = dotIdx > 0 ? dotIdx : sel.length
// 若 id 选择器或 class 选择器存在,则从 0 位开始截取到最小索引值的地位完结,截取出的就是标签名称
// 都不存在间接取 sel 值
const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel
// 依据 tag 名创立 DOM 元素
const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
? api.createElementNS(i, tag)
: api.createElement(tag)
// 设置 id 属性
if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
// 设置 calss 属性
if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
// 判断 children 是否是数组,是数组则遍历 children
if (is.array(children)) {for (i = 0; i < children.length; ++i) {const ch = children[i]
if (ch != null) {// createElm(ch as VNode, insertedVnodeQueue)递归创立子节点
// api.appendChild(A, B)-->A.appendChild(B)将 B 节点插入到指定父节点 A 的子节点列表的开端
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
}
}
// 判断 vnode.text 有没有值
} else if (is.primitive(vnode.text)) {// api.createTextNode(vnode.text)依据 vnode.text 创立文本节点
// api.appendChild(elm, B)-->A.appendChild(B)将文本节点 B 增加到父节点 elm 子节点列表的开端处
api.appendChild(elm, api.createTextNode(vnode.text))
}
const hook = vnode.data!.hook
if (isDef(hook)) {hook.create?.(emptyNode, vnode)
if (hook.insert) {insertedVnodeQueue.push(vnode)
}
}
} else {
// sel 不存在间接创立文本节点
vnode.elm = api.createTextNode(vnode.text!)
}
return vnode.elm
}
diff 算法 – 新旧 vnode 是同一个节点的状况
下面剖析了新旧 vnode 不是同一个虚构节点,那么是同一个虚构节点又该怎么去解决?首先,调用 patchVnode
函数 patchVnode(oldVnode, vnode, insertedVnodeQueue)
,这个函数会对新旧 vnode 进行准确比拟:
① 如果新旧虚构 DOM 对象全等 oldVnode === vnode
,那么不做任何操作,间接返回;
② 而后判断 vnode 是否有文本节点 isUndef(vnode.text)
,如果没有文本节点则判断 oldVnode 与 vnode 有没有子节点 isDef(oldCh) && isDef(ch)
,如果都有子节点且不相等则调用 updateChildren
函数去更新子节点;
③ 如果只有 vnode 有子节点而 oldVnode 有文本节点或没有内容,将 oldVnode 的文本节点置空或不做解决,调用 addVnodes
函数将 vnode 的子节点创立出对应的实在 DOM 并循环插入到父节点下;
④ 如果只有 oldVnode 有子节点而 vnode 没有内容,则间接删除 oldVnode 下的子节点;
⑤ 如果只有 oldVnode 有文本节点而 vnode 没有内容,则将 oldVnode 对应的实在 DOM 节点的文本置空;
⑥ 如果 vnode 有文本节点,oldVnode 有子节点就将对应实在 DOM 节点的子节点删除,没有就不解决,而后将 vnode 的文本节点作为子节点插入到对应实在 DOM 节点下。
局部源码剖析在 patch
函数中,上面贴一下 patchVnode
函数的源码剖析:
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
const hook = vnode.data?.hook
hook?.prepatch?.(oldVnode, vnode)
const elm = vnode.elm = oldVnode.elm!
const oldCh = oldVnode.children as VNode[]
const ch = vnode.children as VNode[]
// oldVnode 与 vnode 齐全相等并没有须要更新的内容则间接返回,不做解决
if (oldVnode === vnode) return
if (vnode.data !== undefined) {for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
vnode.data.hook?.update?.(oldVnode, vnode)
}
// vnode.text 为 undefined 示意 vnode 虚构节点没有文本内容
if (isUndef(vnode.text)) {
// oldCh 与 ch 都不为 undefined 示意 oldVnode 与 vnode 都有虚构子节点 children
if (isDef(oldCh) && isDef(ch)) {
// oldCh !== ch 利用算法去更新子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
} else if (isDef(ch)) {// 将 oldVnode 的文本节点设置为 ''if (isDef(oldVnode.text)) api.setTextContent(elm,'')
// 调用 addVnodes 办法将 vnode 的虚构子节点循环插入到 elm 节点的子列表下
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// oldCh 不为 undefined 示意 oldVnode 有虚构子节点 children
} else if (isDef(oldCh)) {
// vnode 没有 children 则间接删除 oldVnode 的 children
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// oldVnode.text 有值而 vnode.text 没有值
} else if (isDef(oldVnode.text)) {// 将 oldVnode 的文本节点设置为 ''api.setTextContent(elm,'')
}
// oldVnode 与 vnode 文本节点内容不同
} else if (oldVnode.text !== vnode.text) {// isDef(oldCh)-->oldCh !== undefined 表明 oldVnode 虚构节点下有虚构子节点
if (isDef(oldCh)) {removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
// oldCh 虚构节点下没有虚构子节点则间接更新文本内容
api.setTextContent(elm, vnode.text!)
}
hook?.postpatch?.(oldVnode, vnode)
}
diff 算法 – 新旧 vnode 子节点的更新策略
当新旧 vnode 都有子节点时,diff
算法定义了四个指针来解决子节点,四个指针别离是:oldStartVnode(旧前 vnode)
/newStartVnode(新前 vnode)
/oldEndVnode(旧后 vnode)
/newEndVnode(新后 vnode)
。进入循环体内后,新旧 vnode 的子节点两两比拟,这里提供了一套比拟规定,如下图:
如果下面四个规定都不满足,将 oldVnode 的子节点从旧的前索引 oldStartIdx
到旧的后索引 oldEndIdx
做一个 key 与对应地位序号的映射 oldKeyToIdx
,通过新 vnode 的 key 去找 oldKeyToIdx
中是否有对应的索引值,若没有,表明 oldVnode
没有对应的旧节点,是一个新增的节点,进行插入操作;若有,表明 oldVnode
有对应的旧节点,不是一个新增节点,进行挪动操作。上面贴一下源码解析:
// 旧 vnode 的子节点的前索引 oldStartIdx 到后索引 oldEndIdx 的 key 与对应地位序号的映射关系
function createKeyToOldIdx(children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {const map: KeyToIndexMap = {}
for (let i = beginIdx; i <= endIdx; ++i) {const key = children[i]?.key
if (key !== undefined) {map[key] = i
}
}
/**
* 例如:map = {A: 1, B: 2}
*/
return map
}
function updateChildren(parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0 // 旧的前索引
let newStartIdx = 0 // 新的前索引
let oldEndIdx = oldCh.length - 1 // 旧的后索引
let newEndIdx = newCh.length - 1 // 新的后索引
let oldStartVnode = oldCh[0] // 旧的前 vnode
let newStartVnode = newCh[0] // 新的前 vnode
let oldEndVnode = oldCh[oldEndIdx] // 旧的后 vnode
let newEndVnode = newCh[newEndIdx] // 新的后 vnode
let oldKeyToIdx: KeyToIndexMap | undefined
let idxInOld: number
let elmToMove: VNode
let before: any
// 当旧的前索引 <= 旧的后索引 && 新的前索引 <= 新的后索引时执行循环语句
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 为什么 oldStartVnode == null?
// 因为虚构节点进行挪动操作后要将原来的虚构节点置为 undefined 了
// oldCh[idxInOld] = undefined as any
if (oldStartVnode == null) {// oldStartVnode 为 null 就过滤掉以后节点,取 oldCh[++oldStartIdx]节点(旧的前索引的下一个索引的节点)
oldStartVnode = oldCh[++oldStartIdx]
} else if (oldEndVnode == null) {// oldEndVnode 为 null 就过滤掉以后节点,取 oldCh[--oldEndIdx]节点(旧的后索引的上一个索引的节点)
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode == null) {// newStartVnode 为 null 就过滤掉以后节点,取 newCh[++newStartIdx]节点(新的前索引的下一个索引的节点)
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode == null) {// newEndVnode 为 null 就过滤掉以后节点,取 newCh[--newEndIdx]节点(新的后索引的上一个索引的节点)
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
/**
* ① 旧的前 vnode(oldStartVnode)与 新的前 vnode(newStartVnode)比拟是否是同一个虚构节点
* 旧的虚构子节点 新的虚构子节点
* h('li', { key: 'A'}, 'A') h('li', { key: 'A'}, 'A')
* h('li', { key: 'B'}, 'B') h('li', { key: 'B'}, 'B')
*/
// 如果判断是同一个虚构节点则调用 patchVnode 函数
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
// oldCh[++oldStartIdx]取旧的前索引节点的下一个虚构节点(例子中 key 为 B 的节点),赋值给 oldStartVnode
oldStartVnode = oldCh[++oldStartIdx]
// oldCh[++oldStartIdx]取新的前索引节点的下一个虚构节点(例子中 key 为 B 的节点),赋值给 newStartVnode
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
/**
* 如果旧的前 vnode(例子中 key 为 B 的虚构节点)与 新的前 vnode(例子中 key 为 B 的虚构节点)* 不是同一个虚构节点则进行计划②比拟
* ② 旧的后 vnode(oldEndVnode)与 新的后 vnode(newEndVnode)比拟是否是同一个虚构节点
* 旧的虚构子节点 新的虚构子节点
* h('li', { key: 'C'}, 'C') h('li', { key: 'A'}, 'A')
* h('li', { key: 'B'}, 'B') h('li', { key: 'B'}, 'B')
*/
// 如果判断是同一个虚构节点则调用 patchVnode 函数
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
// oldCh[--oldEndIdx]取旧的后索引节点的上一个虚构节点(例子中 key 为 C 的虚构节点),赋值给 oldEndVnode
oldEndVnode = oldCh[--oldEndIdx]
// newCh[--newEndIdx]取新的后索引节点的上一个虚构节点(例子中 key 为 A 的虚构节点),赋值给 newEndVnode
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
/**
* 如果旧的后 vnode 与 新的后 vnode 不是同一个虚构节点则进行计划③比拟
* ③ 旧的前 vnode(oldStartVnode)与 新的后 vnode(newEndVnode)比拟是否是同一个虚构节点
* 旧的虚构子节点 新的虚构子节点
* h('li', { key: 'C'}, 'C') h('li', { key: 'A'}, 'A')
* h('li', { key: 'B'}, 'B') h('li', { key: 'B'}, 'B')
* h('li', { key: 'C'}, 'C')
*/
// 如果判断是同一个虚构节点则调用 patchVnode 函数
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 将旧的前 vnode(相当于例子中 key 为 C 的虚构节点)插入到以后旧的后 vnode 的下一个兄弟节点的后面
// 如果 oldEndVnode 是最开端的虚构节点,则 node.nextSibling 会返回 null,// 则新的虚构节点直接插入到最开端,等同于 appenChild
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
// oldCh[++oldStartIdx]取旧的前索引虚构节点的下一个虚构节点(例子中 key 为 B 的虚构节点),赋值给 oldStartVnode
oldStartVnode = oldCh[++oldStartIdx]
// newCh[--newEndIdx]取新的后索引虚构节点的上一个虚构节点(例子中 key 为 B 的虚构节点),赋值给 newEndVnode
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
/**
* 如果旧的前 vnode 与 新的后 vnode 不是同一个虚构节点则进行计划④比拟
* ④ 旧的后 vnode(oldEndVnode)与 新的前 vnode(newStartVnode)比拟是否是同一个虚构节点
* 旧的虚构子节点 新的虚构子节点
* h('li', { key: 'C'}, 'C') h('li', { key: 'B'}, 'B')
* h('li', { key: 'B'}, 'B') h('li', { key: 'A'}, 'A')
* h('li', { key: 'C'}, 'C')
*/
// 如果判断是同一个虚构节点则调用 patchVnode 函数
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 将旧的后 vnode(例子中 key 为 B)插入到以后旧的前 vnode(例子中 key 为 C)的后面
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
// oldCh[--oldEndIdx]取旧的后索引节点的上一个虚构节点(例子中 key 为 C 的虚构节点),赋值给 oldEndVnode
oldEndVnode = oldCh[--oldEndIdx]
// newCh[++newStartIdx]取新的前索引节点的下一个虚构节点(例子中 key 为 A 的虚构节点),赋值给 newStartVnode
newStartVnode = newCh[++newStartIdx]
} else {
// 不满足以上四种状况
if (oldKeyToIdx === undefined) {
// oldKeyToIdx 保留旧的 children 中各个节点的 key 与对应地位序号的映射关系
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
// 从 oldKeyToIdx 中获取以后 newStartVnode 节点 key 对应的序号
idxInOld = oldKeyToIdx[newStartVnode.key as string]
if (isUndef(idxInOld)) {// isUndef(idxInOld) --> idxInOld === undefined
/**
* idxInOld = undefined 要插入节点
* 旧的虚构子节点中没有 idxInOld 对应的节点,而新的虚构子节点中有,* 所以 newStartVnode 是须要插入的虚构节点
* 旧的虚构子节点 新的虚构子节点
* h('li', { key: 'A'}, 'A') h('li', { key: 'C'}, 'C')
* h('li', { key: 'B'}, 'B')
*/
// 依据 newStartVnode(例子中 key 为 C 的虚构节点)创立实在 DOM 节点 createElm(),// 将创立的 DOM 节点插入到 oldStartVnode.elm(例子中 key 为 A 的节点)的后面
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
/**
* idxInOld != undefined 要挪动节点
* 旧的虚构子节点中有 idxInOld 对应的节点,所以 oldCh[idxInOld]是须要挪动的虚构节点
* 旧的虚构子节点 新的虚构子节点
* h('li', { key: 'A'}, 'A') h('div', { key: 'B'}, 'B')
* h('li', { key: 'B'}, 'B') h('li', { key: 'D'}, 'D')
*/
elmToMove = oldCh[idxInOld] // elmToMove 保留要挪动的虚构节点
// 判断 elmToMove 与 newStartVnode 在 key 雷同的状况下 sel 属性是否雷同
if (elmToMove.sel !== newStartVnode.sel) {
// sel 属性不雷同表明不是同一个虚构节点,// 依据 newStartVnode 虚构节点创立实在 DOM 节点并插入到 oldStartVnode.elm(旧的 key 为 A 的节点)之前
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
// key 与 sel 雷同示意是同一个虚构节点,调用 patchVnode 函数
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
// 解决完被挪动的虚构节点 oldCh[idxInOld]要设置为 undefined,不便下次循环解决时过滤掉曾经解决的节点
oldCh[idxInOld] = undefined as any
// 将 elmToMove.elm(例子中旧的 key 为 B 的节点)插入到 oldStartVnode.elm(例子中 key 为 A 的节点)的后面
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
}
}
// 取 newCh[++newStartIdx]虚构节点(例子中 key 为 D 的虚构节点)赋值给 newStartVnode
newStartVnode = newCh[++newStartIdx]
}
}
/**
* 循环完结后旧的前索引 <= 旧的后索引 || 新的前索引 <= 新的后索引,* 示意还有局部虚构节点(例子中 key 为 C 的虚构节点)没解决
* 旧的虚构子节点 新的虚构子节点
* 状况一:* h('li', { key: 'A'}, 'A') h('li', { key: 'A'}, 'A')
* h('li', { key: 'B'}, 'B') h('li', { key: 'B'}, 'B')
* h('li', { key: 'D'}, 'D') h('li', { key: 'C'}, 'C')
* h('li', { key: 'D'}, 'D')
* 状况二:* h('li', { key: 'A'}, 'A') h('li', { key: 'A'}, 'A')
* h('li', { key: 'B'}, 'B') h('li', { key: 'B'}, 'B')
* h('li', { key: 'C'}, 'C')
*/
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
// 解决例子中状况一
if (oldStartIdx > oldEndIdx) {// 待插入的节点以 before 节点为参照,newCh[newEndIdx]是例子中新的子节点中 key 为 C 的虚构节点,// 所以 before = newCh[newEndIdx + 1]是 key 为 D 的虚构节点
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
// 例子中当初 newStartIdx,newEndIdx 都为 2
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else {
// 解决例子中状况二,删除旧的前索引到旧的后索引两头的节点(例子中删除旧的 key 为 C 的虚构节点)removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
}