- 前置概念
- 辅助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.children
const tag = vnode.tag
if (isDef(tag)) { // 是否是 普通节点
vnode.elm = nodeOps.createElement(tag, vnode) // 创建DOM元素
createChildren(vnode, children) // 创建子节点,并将子节点插入 vnode.elm
insert(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) {
return
a.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 true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return 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].key
if (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.elm
const 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 i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const 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 = 0
let newStartIdx = 0
// 新旧 VNode 尾指针
let oldEndIdx = oldCh.length - 1
let 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, refElm
const canMove = !removeOnly
while (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 则进行 patchVnode
patchVnode(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].elm
addVnodes(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 旧头 == 新头
```javascript
sameVnode(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 **前面
```javascript
createElm(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) }
- 再更新 新头节点 索引
```javascript
newStartVnode = 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 比较,不同 则 新增、删除