关于前端:虚拟-DOM-与-Diff-算法的实现原理

58次阅读

共计 15560 个字符,预计需要花费 39 分钟才能阅读完成。

前言

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 对象后就能够进行差别比拟了。在理论应用过程中,咱们是间接调用 snabbdompatch 函数,而后传入两个参数,通过 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)
}
}
}

正文完
 0