双端 Diff 算法指的是,在新旧两组子节点的四个端点之间分别进行比较, 并试图找到可复用的节点。相比简单 Diff 算法,双端 Diff 算法的优势 在于,对于同样的更新场景,执行的 DOM 移动操作次数更少。
import { patchKeyedChildren } from './doubleEndDiff.js';
import cloneFn from '../../CloneDeep/forClone.js'; // 引入深拷贝
// 新旧节点测试数据
const oldNode = {
type: 'div',
children: [
{
type: 'p',
children: '1',
key: 1
},
{
type: 'p',
children: '2',
key: 2
},
{
type: 'p',
children: '3',
key: 3
},
{
type: 'p',
children: '5',
key: 5
},
]
}
const newNode = {
type: 'div',
children: [
{
type: 'p',
children: '2',
key: 2
},
{
type: 'p',
children: '4',
key: 4
},
{
type: 'p',
children: '3',
key: 3
},
{
type: 'p',
children: '1',
key: 1
},
{
type: 'p',
children: '6',
key: 6
}
]
}
const oldNodeCopy = cloneFn(oldNode);
const {moveRecord} = patchKeyedChildren(oldNode.children, newNode.children, oldNodeCopy.children);
console.log(moveRecord);
/**
[
'move : 2-> 1 before',
'insert : 4 -> 1 before',
'move : 3-> 1 before',
'1 stay',
'insert : 6 -> 5 before',
'delete 5'
]
*/
let moveRecord = [] // 存储变化操作
/**
* @desc
* @param {*} n1 旧节点
* @param {*} n2 新节点
* @param {*} container 容器(旧节点的深拷贝,或者是真实 DOM )
*/
function patchKeyedChildren(n1, n2, container) {
const oldChildren = n1;
const newChildren = n2;
// 创建4个索引,分别指向新旧节点的头尾;
let oldStartIdx = 0, newStartIdx = 0;
let oldEndIdx = oldChildren.length - 1;
let newEndIdx = newChildren.length - 1;
// 创建4个指针,指向新旧节点的头尾
let oldStartNode = oldChildren[oldStartIdx]
let oldEndNode = oldChildren[oldEndIdx]
let newStartNode = newChildren[newStartIdx]
let newEndNode = newChildren[newEndIdx];
/**
* 4次比较:头头、尾尾、新尾旧头、新头旧尾
*/
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 若存在undefined,直接跳过(旧节点中被删除了的)
if (!oldStartNode) {
oldStartNode = oldChildren[++oldStartIdx]
}
else if (!oldEndNode) {
oldEndNode = oldChildren[--oldEndIdx]
}
// 四次比较中存在可复用节点,插入操作记录
else if (oldStartNode.key === newStartNode.key) {
// 新旧节点的头部key 相同,向下移动指针
moveRecord.push(`${newStartNode.key} stay`)
newStartNode = newChildren[++newStartIdx]
oldStartNode = oldChildren[++oldStartIdx]
} else if (oldEndNode.key === newEndNode.key) {
// 尾部 key 相同,向上移动指针
moveRecord.push(`${oldEndNode.key} stay`)
newEndNode = newChildren[--newEndIdx]
oldEndNode = oldChildren[--oldEndIdx]
} else if (newEndNode.key === oldStartNode.key) {
// 新尾旧头 相同,在容器中移动 旧头部数据 到 旧尾部数据前面,移动指针
const operateDetail = insert(oldStartNode, container, oldEndNode);
moveRecord.push(operateDetail)
newEndNode = newChildren[--newEndIdx]
oldStartNode = oldChildren[++oldStartIdx]
} else if (newStartNode.key === oldEndNode.key) {
// 新头旧尾 相同,在容器中移动 旧尾部数据 到 旧头部数据前面,移动指针
const operateDetail = insert(oldEndNode, container, oldStartNode);
moveRecord.push(operateDetail);
newStartNode = newChildren[++newStartIdx]
oldEndNode = oldChildren[--oldEndIdx]
} else {
// 四次比较不存在相同部分。
}
}
}
function patchKeyedChildren(n1, n2, container) {
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
if(...){...}
else if (...){...}
....
else {
/**
* 新旧头尾未找到可复用节点,在旧节点中寻找是否存在新头节点
* 若存在:在容器中将其插入到 oldStartNode 前面,并在旧节点中将其设置为 undefined,
* 若不存在:直接将该 newStartNode 插入到容器的 oldStartNode 前面
* 再移动下标继续遍历
*/
const idxInOld = oldChildren.findIndex((item) => {
if (item ? (item.key === newStartNode.key) : false) {
return true
}
return false
})
if (idxInOld > 0) {
const vnodeToMove = oldChildren[idxInOld]
const operateDetail = insert(vnodeToMove, container, oldStartNode);
moveRecord.push(operateDetail);
oldChildren[idxInOld] = undefined;
} else {
const operateDetail = insert(newStartNode, container, oldStartNode);
moveRecord.push(operateDetail);
}
newStartNode = newChildren[++newStartIdx]
}
}
}
function patchKeyedChildren(n1, n2, container) {
//.....
/**
* 当旧节点循环完毕,新节点还有剩余,说明要新增
* 反之说明要删除
*/
if (newStartIdx <= newEndIdx && oldStartIdx > oldEndIdx) {
for (let i = newStartIdx; i <= newEndIdx; i++) {
const operateDetail = insert(newStartNode, container, oldStartNode);
moveRecord.push(operateDetail);
}
} else if (newStartIdx > newEndIdx && oldStartIdx <= oldEndIdx) {
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const operateDetail = unmounted(container, oldChildren[i])
moveRecord.push(operateDetail);
}
}
return { n1, n2, container, moveRecord }
}
/**
* @desc 节点插入,若moveNode 不存在旧节点中,则插入为新增,存在则删除后插入,为移动。若targetNode不传则push到末尾
* @param {*} moveNode 要移动的节点
* @param {*} container 节点容器
* @param {*} targetNode 移动的目标位置
* @returns 操作详情
*/
function insert(moveNode, container, targetNode) {
const oldIdx = findIdxInContainer(moveNode, container);
if (oldIdx >= 0) {
container.splice(oldIdx, 1)
}
const targetIdx = findIdxInContainer(targetNode, container);
targetIdx > 0 ? container.splice(targetIdx, 0, moveNode) : container.push(moveNode);
return oldIdx >= 0 ?
`move : ${moveNode.key}-> ${targetNode.key} before` :
`insert : ${moveNode.key} -> ${targetNode ? targetNode.key + ' before' : 'last'}`
}
/**
* 将节点和容器数据分别转成 JSON
* @param {*} container 容器
* @param {*} unMountedNode 要删除的节点
* @returns 删除记录
*/
function unmounted(container, unMountedNode) {
const targetIdx = container.indexOf(unMountedNode);
if (targetIdx >= 0) {
container.splice(targetIdx, 1);
}
return `delete ${unMountedNode.key}`
}
/**
* @desc 将数据转为JSON后返回 vnode 在 container 中的 idx
* @param {*} vnode 对象节点
* @param {*} container 容器
* @returns 节点位置idx
*/
function findIdxInContainer(vnode, container) {
const vnodeStr = JSON.stringify(vnode)
const containerStr = container.map((item) => {
return JSON.stringify(item)
})
return containerStr.indexOf(vnodeStr);
}
let moveRecord = [] // 存储变化操作
/**
* @desc 1. 两节点为字符串,则使用 LCS 算法求字符串的diff
* 2. 创建4个索引和指针,分别指向新旧节点的头尾,进行4次比较:头头、尾尾、新尾旧头、新头旧尾;
* 1. 若四次比较中存在可复用节点,则移动对应的索引和指针
* 2. 新旧头尾未找到可复用节点,在旧节点中寻找是否存在新头节点
* 1. 若存在:在容器中将其插入到 oldStartNode 前面,并在旧节点中将其设置为 undefined
* 2. 若不存在:直接将该 newStartNode 插入到容器的 oldStartNode 前面
* 3. 再移动下标继续遍历
* 3. 若存在指针为 undefined,即2.2。1中已经遍历过的节点,则直接指针移动跳过
* 3. 当旧节点循环完毕,新节点还有剩余,说明要新增。反之说明要删除
*
* @param {*} n1 旧节点
* @param {*} n2 新节点
* @param {*} container 容器(旧节点的深拷贝,或者是真实 DOM )
*/
function patchKeyedChildren(n1, n2, container) {
const oldChildren = n1;
const newChildren = n2;
// 创建4个索引,分别指向新旧节点的头尾;
let oldStartIdx = 0, newStartIdx = 0;
let oldEndIdx = oldChildren.length - 1;
let newEndIdx = newChildren.length - 1;
// 创建4个指针,指向新旧节点的头尾
let oldStartNode = oldChildren[oldStartIdx]
let oldEndNode = oldChildren[oldEndIdx]
let newStartNode = newChildren[newStartIdx]
let newEndNode = newChildren[newEndIdx];
/**
* 4次比较:头头、尾尾、新尾旧头、新头旧尾
*/
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 若存在undefined(旧节点中被删除了的)
if (!oldStartNode) {
oldStartNode = oldChildren[++oldStartIdx]
}
else if (!oldEndNode) {
oldEndNode = oldChildren[--oldEndIdx]
}
// 四次比较中存在可复用节点
else if (oldStartNode.key === newStartNode.key) {
moveRecord.push(`${newStartNode.key} stay`)
newStartNode = newChildren[++newStartIdx]
oldStartNode = oldChildren[++oldStartIdx]
} else if (oldEndNode.key === newEndNode.key) {
moveRecord.push(`${oldEndNode.key} stay`)
newEndNode = newChildren[--newEndIdx]
oldEndNode = oldChildren[--oldEndIdx]
} else if (newEndNode.key === oldStartNode.key) {
const operateDetail = insert(oldStartNode, container, oldEndNode);
moveRecord.push(operateDetail)
newEndNode = newChildren[--newEndIdx]
oldStartNode = oldChildren[++oldStartIdx]
} else if (newStartNode.key === oldEndNode.key) {
const operateDetail = insert(oldEndNode, container, oldStartNode);
moveRecord.push(operateDetail);
newStartNode = newChildren[++newStartIdx]
oldEndNode = oldChildren[--oldEndIdx]
} else {
/**
* 新旧头尾未找到可复用节点,在旧节点中寻找是否存在新头节点
* 若存在:在容器中将其插入到 oldStartNode 前面,并在旧节点中将其设置为 undefined,
* 若不存在:直接将该 newStartNode 插入到容器的 oldStartNode 前面
* 再移动下标继续遍历
*/
const idxInOld = oldChildren.findIndex((item) => {
if (item ? (item.key === newStartNode.key) : false) {
return true
}
return false
})
if (idxInOld > 0) {
const vnodeToMove = oldChildren[idxInOld]
const operateDetail = insert(vnodeToMove, container, oldStartNode);
moveRecord.push(operateDetail);
oldChildren[idxInOld] = undefined;
} else {
const operateDetail = insert(newStartNode, container, oldStartNode);
moveRecord.push(operateDetail);
}
newStartNode = newChildren[++newStartIdx]
}
}
/**
* 当旧节点循环完毕,新节点还有剩余,说明要新增
* 反之说明要删除
*/
if (newStartIdx <= newEndIdx && oldStartIdx > oldEndIdx) {
for (let i = newStartIdx; i <= newEndIdx; i++) {
const operateDetail = insert(newStartNode, container, oldStartNode);
moveRecord.push(operateDetail);
}
} else if (newStartIdx > newEndIdx && oldStartIdx <= oldEndIdx) {
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const operateDetail = unmounted(container, oldChildren[i])
moveRecord.push(operateDetail);
}
}
return { n1, n2, container, moveRecord }
}
/**
* @desc 节点插入,若moveNode 不存在旧节点中,则插入为新增,存在则删除后插入,为移动。若targetNode不传则push到末尾
* @param {*} moveNode 要移动的节点
* @param {*} container 节点容器
* @param {*} targetNode 移动的目标位置
* @returns 操作详情
*/
function insert(moveNode, container, targetNode) {
const oldIdx = findIdxInContainer(moveNode, container);
if (oldIdx >= 0) {
container.splice(oldIdx, 1)
}
const targetIdx = findIdxInContainer(targetNode, container);
targetIdx > 0 ? container.splice(targetIdx, 0, moveNode) : container.push(moveNode);
return oldIdx >= 0 ?
`move : ${moveNode.key}-> ${targetNode.key} before` :
`insert : ${moveNode.key} -> ${targetNode ? targetNode.key + ' before' : 'last'}`
}
/**
* 将节点和容器数据分别转成 JSON
* @param {*} container 容器
* @param {*} unMountedNode 要删除的节点
* @returns 删除记录
*/
function unmounted(container, unMountedNode) {
const targetIdx = container.indexOf(unMountedNode);
if (targetIdx >= 0) {
container.splice(targetIdx, 1);
}
return `delete ${unMountedNode.key}`
}
/**
* @desc 将数据转为JSON后返回 vnode 在 container 中的 idx
* @param {*} vnode 对象节点
* @param {*} container 容器
* @returns 节点位置idx
*/
function findIdxInContainer(vnode, container) {
const vnodeStr = JSON.stringify(vnode)
const containerStr = container.map((item) => {
return JSON.stringify(item)
})
return containerStr.indexOf(vnodeStr);
}