前言
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): VNodeexport function h(sel: string, data: VNodeData | null): VNodeexport function h(sel: string, children: VNodeChildren): VNodeexport function h(sel: string, data: VNodeData | null, children: VNodeChildren): VNodeexport 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] } // 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) } } }