- 前置概念
- 辅助API
- 比较过程
- createPatchFunction
- patchVnode
- Vnode 是文本节点。通过 text 属性 就可以判定是 文本节点,所以就有两种处理方式。
- Vnode 存在子节点。因为不知道新旧子节点是否一样,所有这里有三种情况。
- 数据对象、也有考虑到进行相应处理。">除了以上两种情况,还对 注释节点、 数据对象、也有考虑到进行相应处理。
- updateChildren
- 为什么这么比较?
- 流程
前置概念
Diff 比较目的是什么?
实际上创建一个 DOM 添加到页面上显示,其渲染过程是非常复杂的。通过 Diff 比较,找出最小差异 **VNode(虚拟DOM)**,把最小差异那部分 VNode 更新到 DOM 上尽可能减少 DOM 更新渲染消耗,也可理解成是为了复用 DOM 。
Diff 比较做法是什么?
只有两个 新旧 **VNode(虚拟DOM)** 相等时,才会比较 VNode它的子节点,也叫 **同层级比较**。
什么是同层级比较?

上图 总共有 四次 比较
- 第一次,绿色方框 相等,拿到 1 子节点,往下比较
- 第二次,蓝色方框 旧节点 2 跟 新节点 2 相等,拿到 2 子节点,往下比较
- 第三次,蓝色方框 旧节点 3 跟 新节点 3 相等,拿到 3 子节点,往下比较

上图 只有 二次 比较
- 第一次,绿色方框 相等,拿到 1 子节点,往下比较
- 第二次,蓝色方框 旧节点 跟 新节点 没有相等,则不会再往下比较
总结
- Diff 比较本质上是为了 复用 DOM 节点,所以要在找 旧节点 中找出 相同的节点
- 比较建立在 同层比较基础之上的,而不是为了找到相同节点,无限制递归查找
辅助API
下面是在 Diff 比较过程中 所需要用到的一些方法辅助,比如 创建DOM元素、比较节点是否相等 等等…
跨平台 nodeOps
Diff 比较之后如何更新到页面呢?
在 **Web浏览器 **环境里,要操作 **真实DOM **只能是调用 浏览器提供的一些 API 例如:`**appendChild**`,所以比较的过程中 更新真实DOM,实际上还是调用 浏览器提供的 **原生API。**
除了 浏览器环境还会有别的环境 吗?
当然!比如说 **weex**,由于使用了 **Virtual DOM **的原因,Vue.js具有了跨平台的能力,**Virtual DOM** 终归只是用来描述 真实DOM 一些 JavaScript 对象罢了。那么就需要有一个 适配层 ,来适配不同平台所需要调用 API 的差异,将不同平台的 API 封装在内,统一对外提供。如下面代码:
const nodeOps = {setTextContent (text) {if (platform === 'weex') {node.parentNode.setAttr('value', text);} else if (platform === 'web') {node.textContent = text;}},parentNode () {//......},removeChild () {//......},nextSibling () {//......},insertBefore () {//......},createElement () {// .......}}
insert 插入节点
function insert (parent, elm, ref) {if (isDef(parent)) { // 是否有 父节点,需要根据父节点插入节点if (isDef(ref)) { // 是否有传 参考兄弟节点if (nodeOps.parentNode(ref) === parent) { // 父节点是否相同,相同才是兄弟节点nodeOps.insertBefore(parent, elm, ref) // 将 elm 节点,插在兄弟节点前面}} else { // 没有 直接插入父节点末尾nodeOps.appendChild(parent, elm)}}}
- 在 Diff 比较的过程会使用该方法 将 DOM 节点插入到页面
creteElm 创建节点
function createElm (vnode, parentElm, refElm){const children = vnode.childrenconst tag = vnode.tagif (isDef(tag)) { // 是否是 普通节点vnode.elm = nodeOps.createElement(tag, vnode) // 创建DOM元素createChildren(vnode, children) // 创建子节点,并将子节点插入 vnode.elminsert(parentElm, vnode.elm, refElm) // 插入 vnode.elm 到页面} else if (isTrue(vnode.isComment)) { // 是否是 注释节点vnode.elm = nodeOps.createComment(vnode.text)insert(parentElm, vnode.elm, refElm) // 插入 注释节点} else { // 文本节点vnode.elm = nodeOps.createTextNode(vnode.text)insert(parentElm, vnode.elm, refElm) // 插入 文本节点}}
- 文本、注释 VNode 都是没有 tag 属性的,因此用它来判断是不是 普通标签。
- 文本节点:
createChildren 创建子节点
function createChildren (vnode, children) {if (Array.isArray(children)) { // 如果是数组,则遍历逐个创建for (let i = 0; i < children.length; ++i) {createElm(children[i], vnode.elm)}// // 如果是 string,number,symbol,boolean 其中一个一种类型都以 文本节点创建} else if (isPrimitive(vnode.text)) {nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))}}
removeNode 删除节点
function removeNode (el) {const parent = nodeOps.parentNode(el) // 找到 节点的 父节点if (isDef(parent)) {nodeOps.removeChild(parent, el)}}
【sameVnode】比较 VNode 节点是否相等
function sameVnode (a, b) {returna.key === b.key &&(a.tag === b.tag &&a.isComment === b.isComment &&isDef(a.data) === isDef(b.data) &&sameInputType(a, b))}function sameInputType (a, b) {if (a.tag !== 'input') return truelet iconst typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.typeconst typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.typereturn typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)}
- 主要判断四个点,key、tag、双方是否都存在 data、双方都是注释节点
- key
a.undefined === b.test为不相等,a.test === b.test为相等,a.undefined === b.undefined为相等 - tag
a.div === b.span不相等,a.div === b.div相等 - data
a.undefined === b.data不相等(这里的 data 不是数据的 data) - 还有一种情况 input 节点,如果是 input 就比较 type 是否相等
为什么这么判断两个 VNode 是否相等呢?
- 判断目的是 是要重新创建还是复用原 DOM
- data 判断是双方都有 data 对象,那么就是相等的节点,一个有一个没有那肯定不是同一个节点,因为在模版编译时就已经确定是否有 data 了,即便属性是动态的
- key 标识 和 tag 也是在 模版编译时 就知道是不是相同的节点
createKeyToOldIdx 生成 children(子节点)key 对应 index(索引)map
function createKeyToOldIdx (children, beginIdx, endIdx) {let i, key/*** map 表* 结构:[{tag: 'div', key: 'id123'}, {tag: 'p', key: 'id456'}] -> {"id123": 0, "id456": 1}*/const map = {}for (i = beginIdx; i <= endIdx; ++i) {key = children[i].keyif (isDef(key)) map[key] = i // 只有节点有 key 都会存入 map 表}return map}
函数作用是什么?
前面提到 Diff 比较其实也是为了复用 DOM,假设有 大量 children(子节点)逐个比较下来 时间复杂度是 O(n2) 。这个时候就需要有个 id 来标识出两个节点是否相等,而 Key 就是这个标识。
那这样 比较 Key 相等 逐个比较下来不也是 O(n2) 复杂度吗?此时 该函数作用就体现出来 了,比较时 给 旧节点 生成(只生成一次) **key: index** 的 map 表,再拿 新节点的 Key 从 map 查找否存在 相同的 Key,如果存在 就能直接取到 相同Key 的 老节点 Index,再进行具体的比较,这样查找则变成 时间复杂 O(1)。
如果没有 Key ,还是会从老节点中逐个比较查找,从上面也能回答出 key 的作用
总结:key 和 createKeyToOldIdx 作用是为了在 新老节点 比较过程中 降低查找相同节点的复杂度。
比较过程
createPatchFunction
import * as nodeOps from 'web/runtime/node-ops'import platformModules from 'web/runtime/modules/index'const modules = platformModules.concat(baseModules)const createPatchFunction = function createPatchFunction (backend) {// ...return path(oldVnode, vnode) {// 没有旧节点if (isUndef(oldVnode)) {// oldVnode 未定义的时候,就是root节点,创建一个新的节点createElm(vnode, insertedVnodeQueue)} else {// oldVnode 是真实DOM 才为 true// nodeType 属性只有是真实 DOM 才有const isRealElement = isDef(oldVnode.nodeType)// 判断是真实 DOM 且 新旧VNode 相同if( !isRealElement && sameVnode(oldVnode, vnode)) {patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)} else {// 新旧节点不一样// 创建一个空的将 elm 保存在内oldVnode = emptyNodeAt(oldVnode)const oldElm = oldVnode.elmconst parentElm = nodeOps.parentNode(oldElm)// 创建新节点createElm(vnode, parentElm, nodeOps.nextSibling(oldElm))// 销毁老节点if (isDef(parentElm)) {removeVnodes([oldVnode], 0, 0)}}}}}const path = createPatchFunction({nodeOps,modules,LONG_LIST_THRESHOLD: 10})Vue.prototype.__patch__ = patch
1 没有旧节点
没有旧节点时,说明页面是初始化的时候,此时,不需要比较,直接使用 createElm 全部新建
2 旧节点 和 新节点 一样
通过 sameVnode 对比是否一样,当为 ture 时 直接调用 patchVnode 去比较 子节点
3 旧节点 和 新节点 不一样
不一样,直接使用 createElm 创建一个新节点,接着再删除 旧节点
patchVnode
function patchVnode (oldVnode, vnode){if (oldVnode === vnode) return;const elm = vnode.elm = oldVnode.elm// 新旧节点 都是注释节点if (isTrue(vnode.isStatic) && // 新节点 是注释节点isTrue(oldVnode.isStatic) && // 旧节点 是注释节点vnode.key === oldVnode.key && // 新旧节点 key 相等(isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) // 新节点 是克隆 或者 有标记 v-once 属性) {vnode.componentInstance = oldVnode.componentInstance // 直接使用 旧节点 组件实例见赋值到 新节点组件实例return}// 数据对象 更新let iconst data = vnode.dataif (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {i(oldVnode, vnode)}const oldCh = oldVnode.childrenconst ch = vnode.children// Vnode 是否 文本节点if (isUndef(vnode.text)) {// 都有 子节点if (isDef(oldCh) && isDef(ch)) {if (oldCh !== ch) updateChildren(elm, oldCh, ch)// 只有 新节点 有子节点} else if (isDef(ch)) {for (let startIdx = 0; startIdx <= ch.length-1; ++startIdx) {createElm(vnodes[startIdx], parentElm, refElm)}// 只有 旧VNode 有子节点} else if (isDef(oldCh)) {for (let startIdx = 0; startIdx <= oldCh.length - 1; ++startIdx) {removeNode(oldCh[startIdx].elm)}// 旧节点 有文本节点} else if (isDef(oldVnode.text)) {nodeOps.setTextContent(elm, '')}// 新旧 VNode 节点,文本不一致,直接替换文本} else if (oldVnode.text !== vnode.text) {nodeOps.setTextContent(elm, vnode.text)}}
Vnode 是文本节点。通过 text 属性 就可以判定是 文本节点,所以就有两种处理方式。
- 当 Vnode.text 存在,且 和 旧的 Vnode.text 不一样时,直接更新节点内容
nodeOps.setTextContent(elm, vnode.text)
- 当 Vnode.text 不存在,但 旧的 oldVnode.text 存在 时,直接将内容赋值为空
nodeOps.setTextContent(elm, '')
Vnode 存在子节点。因为不知道新旧子节点是否一样,所有这里有三种情况。
只有新子节点。那么这里就没得比较,遍历逐个新建就好了,并把 父子节点也添加进去
只有旧子节点。旧子节点有 而没有新的子节点,说明更新后节点都被删除了,此时 只需遍历通过 removeNode 逐个把旧子节点 删除。
新旧节点 都有子节点,而且不一样。当出现这种情况就不是单个比较了,而是通过
updateChildren方法遍历逐个进行比较。
除了以上两种情况,还对 注释节点、 数据对象、也有考虑到进行相应处理。
注释节点,如果 新旧节点都是注释节点 且 节点 key 相等,则直接使用 旧节点实例
componentInstance结束比较。数据对象,包含 class、style、attrs 等等,在其内部也调用对应的方法进行比较更新 如:updateClass、updateAttrs 等等,当然上面源码没有呈现出来,源码路径:
src\platforms\web\runtime\modules。
updateChildren
function updateChildren (parentElm, oldCh, newCh, removeOnly) {// 新旧 VNode 首指针let oldStartIdx = 0let newStartIdx = 0// 新旧 VNode 尾指针let oldEndIdx = oldCh.length - 1let newEndIdx = newCh.length - 1// 新旧 VNode 首节点let oldStartVnode = oldCh[0]let newStartVnode = newCh[0]// 新旧 VNode 尾节点let oldEndVnode = oldCh[oldEndIdx]let newEndVnode = newCh[newEndIdx]let oldKeyToIdx, idxInOld, vnodeToMove, refElmconst canMove = !removeOnlywhile (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (isUndef(oldStartVnode)) {// 旧VNode 首节点 不存在,旧节点 指针右移oldStartVnode = oldCh[++oldStartIdx]} else if (isUndef(oldEndVnode)) {// 旧VNode 尾节点 不存在,旧节点 指左针移oldEndVnode = oldCh[--oldEndIdx]} else if (sameVnode(oldStartVnode, newStartVnode)) {// 新旧 VNode 首节点 一致,首首比较patchVnode(oldStartVnode, newStartVnode, newCh, newStartIdx)// 新旧 VNode 指针右移oldStartVnode = oldCh[++oldStartIdx]newStartVnode = newCh[++newStartIdx]} else if (sameVnode(oldEndVnode, newEndVnode)) {// 新旧 VNode 尾节点 一致,尾尾比较patchVnode(oldEndVnode, newEndVnode, newCh, newEndIdx)// 新旧 VNode 指针左移oldEndVnode = oldCh[--oldEndIdx]newEndVnode = newCh[--newEndIdx]} else if (sameVnode(oldStartVnode, newEndVnode)) {// 旧 VNode 首节点 VS 新 VNode 尾节点 一致,首尾 交叉比较patchVnode(oldStartVnode, newEndVnode, newCh, newEndIdx)canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))// 旧 VNode 指针右移oldStartVnode = oldCh[++oldStartIdx]// 新 VNode 指针左移newEndVnode = newCh[--newEndIdx]} else if (sameVnode(oldEndVnode, newStartVnode)) {// 旧 VNode 尾节点 VS 新 VNode 首节点 一致,尾首 交叉比较patchVnode(oldEndVnode, newStartVnode, newCh, newStartIdx)canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)// 旧 VNode 指针左移oldEndVnode = oldCh[--oldEndIdx]// 新 VNode 指针右移newStartVnode = newCh[++newStartIdx]} else {/*** 生成一个 VNode.key :VNdoe 索引位置 对应的哈希表(只有第一次进来oldKeyToIdx = undefined 的时候会生成,也为后面检测重复的key值做铺垫)比如 childre 是这样的 [{tag: 'div', key: 'id123'},{tag: 'div', key: 'id456'}] beginIdx = 0 endIdx = 2结果生成{id123: 0, id456: 1}*/if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)// 判断 新VNode 首节点 是否有 key,有 则拿此 key 去找 旧VNode key哈希表(oldKeyToIdx) 找到 旧VNode 位置// 没有 拿 新VNode 首节点,逐个跟 旧 VNode 比较,是否跟 新VNode 相同节点位置idxInOld = isDef(newStartVnode.key)? oldKeyToIdx[newStartVnode.key]: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)if (isUndef(idxInOld)) {// 没有从 旧VNode 找到相同节点,则创建一个新的节点createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)} else {// 获取同key 的老节点vnodeToMove = oldCh[idxInOld]if (sameVnode(vnodeToMove, newStartVnode)) {// 如果 新VNode 与得到的有相同 key的 旧VNode 节点 是同一个 VNode 则进行 patchVnodepatchVnode(vnodeToMove, newStartVnode, newCh, newStartIdx)oldCh[idxInOld] = undefined// 当有标识位 canMov e实可以直接插入 oldStartVnode 对应的真实 DOM 节点前面canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)} else {// same key but different element. treat as new element// 当 新VNode 与找到的 同样key的 旧VNode 不是 sameVNode 的时候(比如说tag不一样或者是有不一样type的input标签)// 创建一个新的节点createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)}}// 新首 VNode 指针右移动newStartVnode = newCh[++newStartIdx]}}if (oldStartIdx > oldEndIdx) {// 全部比较完成以后,发现 oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多,所以这时候多出来的新节点需要一个一个创建出来加入到真实Dom中refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elmaddVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx)} else if (newStartIdx > newEndIdx) {// 如果全部比较完成以后发现 newStartIdx > newEndIdx,则说明新节点已经遍历完了,旧VNode 比较 新VNode 有多出来,将多余的老节点从真实 DOM 中移除removeVnodes(oldCh, oldStartIdx, oldEndIdx)}}
函数处理了什么?
当出现 新子节点 和 旧子节点 都有的情况,在该函数 新旧子节点 进行,循环遍历逐个比较
如何 循环遍历?
使用 while
新旧节点数组 都配置 两个指向首尾索引 如下:
- 新节点两个索引:newStartIdx、newEndIdx
- 旧节点两个索引:oldStartIdx、oldEndIdx
以两边向中间包围的形式 来进行遍历比较
首部子节点比较完毕,startIdx 就加 1
尾部子节点比较完结,endIdx 就减 1
只要其中一个数组遍历完 (startIdx <= endIdx)则结束遍历
主要处理流程分为 两个
- 比较新旧节点 (主要流程)
- 比较完毕,处理剩下的节点
1. 比较新旧节点
注:在比较时 有两个数组,一个 新子节点数组 newCh、一个 旧子节点数组 oldCh
在比较过程中,不会对这两个数组做任何操作,不会添加,也不会删除
而所有比较过程都是 直接操作DOM,如:插入删除节点。
1.1 比较原则 / 比较逻辑
- 比较原则
首先考虑,不移动 DOM,原地复用
其次考虑,移动 DOM,减少新建
最后考虑,新建 / 删除 DOM
总结:能不移动,尽量不移动。不行就移动,实在不行就只能新建
- 比较 5 种逻辑 ``` 1、旧头 == 新头
2、旧尾 == 新尾
3、旧头 == 新尾
4、旧尾 == 新头
5、单个查找
<a name="SgGDh"></a>#### 1.2 旧头 == 新头```javascriptsameVnode(oldStartVnode, newStartVnode)
- 当两个新旧的两个头一样的时候,并不用做什么处理
- 也符合第一个原则,原地复用 DOM
- 通过 patchVnode 继续处理 两个相同节点的 子节点,或者更新文本
- 因为不考虑多层DOM 结构,所以 新旧两个头一样的话,这里就算结束了
图示:oldStartVnode = oldCh[++oldStartIdx]newStartVnode = newCh[++newStartIdx]

1.3 旧尾 == 新尾
sameVnode(oldEndVnode, newEndVnode)
- 这步比较逻辑 处理相同结果 跟 头头比较是一样的
- 尾尾相同,直接跳入下个循环
图示:oldEndVnode = oldCh[--oldEndIdx]newEndVnode = newCh[--newEndIdx]

1.4 旧头 == 新尾
sameVnode(oldStartVnode, newEndVnode)
这步 节点相同 就不符合,原地复用 了,因为位置不一样,所以只能移动 DOM 了
怎么移动?
先是通过 patchVnode 更新节点信息
再把
oldStartVnode(旧头节点)放到oldEndVnode.elm(旧尾节点)的后面,因为浏览没有提供把 DOM 放到谁后面的方法,所以只能使用insertBefore属性 拿到oldEndVnode.elm下一个节点nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
最后再更新两个索引
oldStartVnode = oldCh[++oldStartIdx]newEndVnode = newCh[--newEndIdx]
图示:

1.5 旧尾 == 新头
sameVnode(oldEndVnode, newStartVnode)
- 同样不能符合 原地复用,只能 移动DOM
怎么移动?
先是通过 patchVnode 更新节点信息
再把
oldEndVnode(旧尾节点)放到oldStartVnode.elm(新头节点)的前面nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
再更新两个索引
oldEndVnode = oldCh[--oldEndIdx]newStartVnode = newCh[++newStartIdx]
图示:
1.6 单个遍历查找
- 当前面四种比较逻辑都不行的时候,这是最后一种处理方法,拿 新头节点,直接去 旧子节点数组中遍历,找一样的节点出来,这里为了 降低查找的复杂度 ,引入了一种方法,在查找之前 先生成,以 旧节点 key 对应 节点 索引 的一个 map 表,大概流程如下: ``` 1、生成旧子节点数组以 vnode.key 为key 的 map 表
2、拿到新子节点数组中 首节点,判断它的key是否在上面的 map 中
3、不存在,则新建DOM
4、存在,继续判断是否 sameVnode
<a name="J0y1b"></a>#####<a name="Xw6fD"></a>##### 1 生成 map 表- 这个 **map **表的作用,就主要是用来判断 **新节点.key** 跟 **旧子节.key 是否有相同,**以此 来判断 **节点是否相同**如 旧节点数组:```javascript[{tag: "div", key: "id123"},{tag: "strong", key: "id456"},{tag: "span", key: "id789"}]
生成 map 表
oldKeyToIdx = {"id123": 0,"id456": 1,"id789": 2}
2. 判断 新节点 是否存在 旧节点数组中
- 根据 新头节点.key 去匹配 map 表,判断是否有相同节点 ```javascript idxInOld = oldKeyToIdx[newStartVnode.key]
if (isUndef(idxInOld)) { // 不存在 } else { // 存在 }
<a name="UP1pc"></a>##### 3 不存在 旧节点数组中- 直接创建**DOM**,并插入**oldStartVnode **前面```javascriptcreateElm(newStartVnode, parentElm, oldStartVnode.elm)
4 存在 旧节点数组中
存在则根据 map 表,找到 旧节点,通过 sameVnode 比较(新头 比较 map 表旧节点)
如果相同,进行 patchVnode 更新节点信息,再将节点移动到 oldStartVnode 前面(因为是拿 新头节点 去比较的)
如果不同,直接创建插入 oldStartVnode 前面 ```javascript vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode) nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { createElm(newStartVnode, parentElm, oldStartVnode.elm) }
- 再更新 新头节点 索引```javascriptnewStartVnode = newCh[++newStartIdx]
2. 处理可能剩下的节点
比较完新旧两个数组之后,可能某个数组会剩下部分节点没有被处理过,所以这里需要统一处理
2.1 新子节点遍历完了
while (newStartIdx <= newEndIdx) {}
- 新子节点 遍历完毕,旧子节点可能还有剩 旧节点,则进行 批量删除!
图示:for (; oldStartIdx <= oldEndIdx; ++oldStartIdx) {oldCh[oldStartIdx].parentNode.removeChild(el);}

2.2 旧子节点遍历完了
while (oldStartIdx <= oldEndIdx) {}
- 旧子节点遍历完毕,新子节点可能有剩,剩余的新子节点 直接 全部新建!
for (; newStartIdx <= newEndIdx; ++newStartIdx) {createElm(newCh[newStartIdx], parentElm, refElm);}
但是有一个问题,就是这些新节点插在哪里?refElm 如何获取?
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
- refElm 获取的是 newEndIdx 旧节点 的后一位节点
如果 newEndIdx+1 的节点如果存在的话,肯定被比较处理过了,那么就逐个添加在 refElm 的前面
如果 newEndIdx 没有移动过,一直是最后一位,那么就 不存在 newCh[newEndIdx + 1]
- 那么 不存在 ,refElm 就是空,剩余的新节点 就全部添加进 末尾。
图示
为什么这么比较?
所有的比较,都是为了找到 新子节点 和 旧子节点 一样的子节点
比较三个原则
1、能不移动,尽量不移动,头头 、尾尾比较
2、没得办法,只好移动,头尾、尾头 交叉比较
3、实在不行,新建或删除,相同 key 比较,不同 则 新增、删除
