diff 算法--快速diff实现

核心思路

  1. 相较于双端diff,快速diff先对新旧节点进行了头尾的预处理,找出头部与尾部开始的相同节点,并对非相同部分进行移动处理
  2. 根据预处理后新节点剩余部分构建数组 source 用于依次存储剩余 newChildred 中的节点在 oldChilren 的 idx,若为新增则值为 -1
  3. 构建一个索引表 keyIdx,存储新节点中的某一个节点对应的在 newChildren 中的 idx,用于填充 source
  4. 遍历 oldChildren 部分,根据 key 值从索引表 keyIdx 中获取此节点在 newChildren 中的idx,存储为 k
    4.1 若k不存在,则说明该旧节点需要删除
    4.2 若k存在,更新 source
  5. 使用 lis 获取 source 的最长子序列 seq。
  6. 循环预处理后的newChilren剩余节点,判断当前位置是否处于 最长子序列(不用移动)上,进行插入或移动

快速 Diff 算法在实测中性能最优。它借鉴了文本 Diff 中的预处理 思路,先处理新旧两组子节点中相同的前置节点和相同的后置节点。 当前置节点和后置节点全部处理完毕后,如果无法简单地通过挂载新 节点或者卸载已经不存在的节点来完成更新,则需要根据节点的索引 关系,构造出一个最长递增子序列。最长递增子序列所指向的节点即 为不需要移动的节点

实现效果

使用代码

import { patchChildrenFast } from './fastDiff.js';
import cloneFn from '../../CloneDeep/forClone.js'; // 引入深拷贝
const fastNewNode = [
    { key: 1 },
    { key: 2 },
    { key: 3 },
    { key: 4 },
    { key: 6 },
    { key: 9 },
    { key: 12 },
    { key: 5 }
]
const fastOldNode = [
    { key: 1 },
    { key: 2 },
    { key: 21 },
    { key: 4 },
    { key: 6 },
    { key: 12 },
    { key: 10 },
    { key: 9 },
    { key: 5 }
]
const { container, moveRecord } = patchChildrenFast(fastOldNode, fastNewNode, cloneFn(fastOldNode));
console.log(moveRecord);
console.log(container);

输出:

moveRecord:
[
  'delete 21',
  'delete 10',
  'move   : 12-> 5 before',
  'insert : 3 -> 4 before'
]
container:
[
  { key: 1 },
  { key: 2 },
  { key: 3 },
  { key: 4 },
  { key: 6 },
  { key: 9 },
  { key: 12 },
  { key: 5 }
]

快速diff:patchChildrenFast函数实现

// 用于返回数组的最长递增子序列的idx数组:传入[1,7,5,3,6,9,2] return [0,3,4,5]
import { lis } from '../../../Alg/Hard/Array/LIS-数组的最长子序列/Lis.js';

let moveRecord = [] // 全局变量数组--存储操作流程
/**
 * @desc  1. 相较于双端diff,快速diff先对新旧节点进行了头尾的预处理,找出头部与尾部开始的相同节点,并对非相同部分进行移动处理
 *        2. 根据预处理后新节点剩余部分构建数组 source 用于依次存储剩余 newChildred 中的节点在 oldChilren 的 idx,若为新增则值为 -1
 *        3. 构建一个索引表 keyIdx,存储新节点中的某一个节点对应的在 newChildren 中的 idx,用于填充 source
 *        4. 遍历 oldChildren 部分,根据 key 值从索引表 keyIdx 中获取此节点在 newChildren 中的idx,存储为 k
 *          4.1 若k不存在,则说明该旧节点需要删除
 *          4.2 若k存在,更新 source 
 *        5. 使用 lis 获取 source 的最长子序列 seq。
 *        6. 循环预处理后的newChilren剩余节点,判断当前位置是否处于 最长子序列(不用移动)上,进行插入或移动
 * @param {*} oldChildren 旧节点
 * @param {*} newChildren 新节点
 * @param {*} container 容器(旧节点的深拷贝,或者是真实 DOM )
 */
export function patchChildrenFast(oldChildren, newChildren, container) {
    if (typeof newChildren == 'string' && typeof oldChildren == 'string') {
	// 若为Str则进行字符串diff
        console.log('Diff String');
    } else if (Array.isArray(newChildren)) {
	// 若为存在key值的对象数组,则进行快速diff
        return patchKeyedChildren(oldChildren, newChildren, container);
    } else {
        console.log('....')
    }
}

patchKeyedChildren实现


/**
 * @desc 1. 先调用 preTreatMent 进行预处理,并返回 preIdx、oldEnd、newEnd,对应非相同部分的开始与结束idx。
 *       2. 若新旧节点中,有其一处理完毕,另一个剩余,则对应将剩余的插入或删除
 *       3. 若新旧节点均为处理完毕,则调用 moveRemainNode 方法进行排序
 *          3.1 构建索引表 keyIdx 存储预处理后 newChildren 中每一个节点,key为节点的key,val为节点的idx
 *          3.2 构建数组 source 存储预处理后 newChildren 中剩余节点在 oldChildren 中的位置
 *          3.3 获取 source 的最长子序列 seq,根据 seq 来进行最少的节点移动
 * @param {*} oldChildren 旧节点
 * @param {*} newChildren 新节点
 * @param {*} container 容器(旧节点的深拷贝,或者是真实 DOM )
 */
function patchKeyedChildren(oldChildren, newChildren, container) {

    // 预处理头尾,除去头尾相同部分
    let { preIdx, oldEnd, newEnd } = preTreatMent(oldChildren, newChildren);

    // 新节点剩余,旧节点处理完毕,需要插入
    if (preIdx <= newEnd && preIdx > oldEnd) {
        while (preIdx <= newEnd) {
            const operateDetail = insert(newChildren[preIdx++], container, oldChildren[oldEnd + 1])
            moveRecord.push(operateDetail)
        }
    }
    // 新节点完毕,旧节点剩余,需要删除
    else if (preIdx > newEnd && preIdx <= oldEnd) {
        while (preIdx <= oldEnd) {
            const operateDetail = unmounted(container, oldChildren[preIdx++])
            moveRecord.push(operateDetail)
        }
    }
    else if(preIdx <= newEnd && preIdx <= oldEnd) {
        // 需要重新排序预处理后的数据
        moveRemainNode(preIdx, oldEnd, newEnd, oldChildren, newChildren, container);
    }
    return {container, moveRecord}
}

preTreatMent实现


/**
 * @desc 用于寻找新旧节点头尾的相同部分,并返回对应的idx,Array[preIdx, xxxEnd]是非头尾相同部分
 * @param {*} oldChildren  旧节点
 * @param {*} newChildren  新节点
 * @returns preIdx: 从 oldChildren[preIdx] 和 newChildren[preIdx] 开始,是不同的
 * @returns oldEnd: 从 oldChildren[oldEnd] 截止是不同的
 * @returns newEnd: 同上
 */
function preTreatMent(oldChildren, newChildren) {
    let preIdx = 0;
    let oldNode = oldChildren[preIdx]
    let newNode = newChildren[preIdx]
    /**
     * 预处理头部
    */
    while (oldNode && newNode && (oldNode.key === newNode.key)) {
        preIdx++;
        oldNode = oldChildren[preIdx]
        newNode = newChildren[preIdx]
    }
    /**
     * 尾部预处理
     * oldEnd < preIdx , newEnd < preIdx若成立,则说明在预处理中已经处理完毕全部旧|新节点
     */
    let oldEnd = oldChildren.length - 1;
    let newEnd = newChildren.length - 1;
    let oldEndNode = oldChildren[oldEnd];
    let newEndNode = newChildren[newEnd];
    while (oldEndNode.key === newEndNode.key) {
        oldEndNode = oldChildren[--oldEnd];
        newEndNode = newChildren[--newEnd];
    }
    return { preIdx, oldEnd, newEnd }
}

moveRemainNode实现


/**
 * @desc  1. 根据预处理后新节点剩余部分构建数组 source 用于依次存储剩余 newChildred 中的节点在 oldChilren 的 idx,若为新增则值为 -1
 *        2. 构建一个索引表 keyIdx,存储新节点中的某一个节点对应的在 newChildren 中的 idx,用于填充 source
 *        3. 遍历 oldChildren 部分,根据 key 值从索引表 keyIdx 中获取此节点在 newChildren 中的idx,存储为 k
 *          3.1 若k不存在,则说明该旧节点需要删除
 *          3.2 若k存在,更新 source 
 *        4. 使用 lis 获取 source 的最长子序列 seq。
 *        5. 循环预处理后的newChilren剩余节点,判断当前位置是否处于 最长子序列(不用移动)上,进行插入或移动
 *         
 * @param {*} preIdx 新旧节点中头部开始不相同部分的开始下标
 * @param {*} oldEnd 尾部不相同部分截止的下标
 * @param {*} newEnd 同上
 * @param {*} oldChildren 旧节点
 * @param {*} newChildren 新节点
 * @param {*} container 修改的容器
 */
function moveRemainNode(preIdx, oldEnd, newEnd, oldChildren, newChildren, container) {
    /**
     * @desc 新节点中预处理后部分的长度
     */
    const count = newEnd - preIdx + 1;

    /**
     *  @desc 存储 newChildren 预处理后剩余部分的每一个节点在 oldChildren 中的idx,-1表示新增节点
     * */ 
    const source = new Array(count).fill(-1);

    const newStartIdx = preIdx;
    const oldStartIdx = preIdx;

    let moved = false;   // 是否需要移动
    let pos = 0;         // 代表 遍历旧的一组子节点的过程中遇到的最大索引值 k。

    /**
     * @desc 创建索引表,新节点中的某一个节点对应的在 newChildren 中的 idx 
     */ 
    const keyIdx = {}

    for (let i = newStartIdx; i <= newEnd; i++) {
        keyIdx[newChildren[i].key] = i;
    }

    let patched = 0; // 已更新过的节点数量
    for (let i = oldStartIdx; i <= oldEnd; i++) {
        /**
         * @desc 遍历 oldChildren 部分,根据 key 值从索引表 keyIdx 中获取此节点在 newChildren 中的idx
         */
        const k = keyIdx[oldChildren[i].key]; 

        // 若更新过的旧节点数量少于等于剩余新节点数量,则更新。反之说明剩余的全部需要卸载,无需比较
        if (patched <= count) {
            // 有 k 说明是可复用节点,并将 i: 该节点在 oldChildren 中的 idx ,赋值给source
            if (typeof k !== 'undefined') {
                source[k - newStartIdx] = i
                // 每更新一个节点,都将 patched 变量 +1
                patched++
                if (k < pos) {
                    moved = true
                } else {
                    pos = k
                }
            }
            // 此节点在 newChildren 中不存在
            else {
                const operateDetail =unmounted(container, oldChildren[i]);
                moveRecord.push(operateDetail)
            }
        }
        // 若更新过的节点数量大于剩余数量,则需要卸载
        else {
            const operateDetail =unmounted(container, oldChildren[i]);
            moveRecord.push(operateDetail)
        }
    }

    if (moved) {
        /**
         * @desc source 的最长子序列
         * @source 存储 newChildren 预处理后剩余部分的每一个节点在 oldChildren 中的idx,-1表示新增节点
         */ 
        const seq = lis(source);

        /**
         * @desc 从末尾开始的最长子序列 idx,会从尾往头递减
         */
        let s = seq.length - 1;
        let i = count - 1;
        // 循环预处理后的newChilren剩余节点,判断当前位置是否处于 最长子序列(不用移动)上
        for (i; i >= 0; i--) {
            // 新增与非最长子序列,则需要插入或移动
            if (source[i] == -1 || i !== seq[s]) {
                // 新增
                let pos = i + newStartIdx
                const nextPos = pos + 1;
                const nextVNode = nextPos < newChildren.length ? newChildren[nextPos] : null;
                const operateDetail = insert(newChildren[pos], container, nextVNode);
                moveRecord.push(operateDetail)
            }
            // 当前位置在 source 的最长子序列上,无需操作
            else if (i === seq[s]) {
                s--
            }

        }
    }
}

插入、卸载函数实现

/**
 * @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);
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值