Vue3渲染器之简单Diff算法

简介: Vue3渲染器之简单Diff算法

diff 算法的目的是根据 key 复用 dom 节点,通过移动节点而不是创建新节点来减少 dom 操作。

由上一节可以看到我们只做同层的对比,type 变了就不再对比子节点

key 属性就像虚拟节点的“身份证”号,只要两个虚拟节点的 type 属性值和 key 属性值都相同,那么我们就认为它们是相同的,即可以进行 DOM 的复用

     let vnode = reactive({
   
            type: 'div',
            children: [
                {
    type: 'p', children: '1', key: 1 },
                {
    type: 'p', children: '2', key: 2 },
                {
    type: 'p', children: '3', key: 3 }
            ]
        })

      vnode.children =
            [
                {
    type: 'p', children: '10', key: 1 },
                {
    type: 'p', children: '20', key: 2 },
                {
    type: 'p', children: '30', key: 3 }
            ]

先从最简单的情况开始(只修改内容),只需要使用双层for循环找出key相同的替换既可

    if (typeof n2.children === 'string') {
   
            // 省略部分代码
        } else if (Array.isArray(n2.children)) {
   
            const oldChildren = n1.children
            const newChildren = n2.children

            // 遍历新的 children
            for (let i = 0; i < newChildren.length; i++) {
   
                const newVNode = newChildren[i]
                // 遍历旧的 children
                for (let j = 0; j < oldChildren.length; j++) {
   
                    const oldVNode = oldChildren[j]
                    // 如果找到了具有相同 key 值的两个节点,说明可以复用,但仍然需要调patch函数更新
                    if (newVNode.key === oldVNode.key) {
   
                        patch(oldVNode, newVNode, container)
                        break // 这里需要 break
                    }
                }
            }
        }

如果位置调换应该怎么做呢

  vnode.children =
            [
                {
    type: 'p', children: '1', key: 1 },
                {
    type: 'p', children: '3', key: 3 }
                {
    type: 'p', children: '2', key: 2 },
            ]

第一步先确定需要移动的元素

  1. 先遍历新数组,根据新数组元素的key在旧数组查找对应key的元素,记录它在旧数组的索引值,这些索引值应该是递增的,如果不是递增代表他要被移动
  2. 需要一个变量记录索引值列表的最大值 let lastIndex = 0
  3. 第一步:在旧数组的索引值0,不小于于lastIndex,不需要移动
  4. 第二步:在旧数组的索引值2,不小于lastIndex,不需要移动,lastIndex更新为2
  5. 第三步:在旧数组的索引值1,小于lastIndex,需要移动,lastIndex不变

总之不是递增的序列就需要移动

let lastIndex = 0
for (let i = 0; i < newLen; i++) {
   
    const newVnode = newChildren[i];
    for (let j = 0; j < oldLen; j++) {
   
        const oldVnode = oldChildren[i];
        if (newVnode.key === oldVnode.key) {
   
            patch(oldVnode, newVnode, container) 
            if (j < lastIndex) {
   
                //进入代表需要移动元素
            } else {
   
                lastIndex = j
            }
            break
        }
    }
}

第二步如何移动

先换一个复杂一点的例子

  vnode.children =
            [
                {
    type: 'p', children: '3', key: 3 },
                {
    type: 'p', children: '1', key: 1 },
                {
    type: 'p', children: '2', key: 2 },
            ]

不难看出需要移动的是第二,三个元素.

  1. 如何移动第二个key为1的元素,可以看出这个元素的位置在新数组是排在key为3之后的,所以只需要把旧数组里的这个元素插入到key为3的元素之后.也就是
  children: [
                {
    type: 'p', children: '2', key: 2 },
                {
    type: 'p', children: '3', key: 3 },
                {
    type: 'p', children: '1', key: 1 },
            ]

2.同理,如何移动第三个key为2的元素,这个元素的位置在新数组是排在key为1之后的,即:

children: [
                {
    type: 'p', children: '3', key: 3 },
                {
    type: 'p', children: '1', key: 1 },
                {
    type: 'p', children: '2', key: 2 }
            ]

使用insertBefore在指定节点之前插入元素,parent是父节点,在这里也就是container容器,el是添加的元素,anchor即指定节点,如果不传anchor插入在最后.

另外这个insertBefore是在DOM下使用的,需要把它当成生成渲染器函数的选项传递,因为不同平台实现api不一样.

parent.insertBefore(el, anchor)

let lastIndex = 0
for (let i = 0; i < newLen; i++) {
   
    const newVnode = newChildren[i];
    for (let j = 0; j < oldLen; j++) {
   
        const oldVnode = oldChildren[i];
        if (newVnode.key === oldVnode.key) {
   
            patch(oldVnode, newVnode, container) 
            if (j < lastIndex) {
   
                // 代码运行到这里,说明 newVNode 对应的真实 DOM 需要移动
                // 先获取 newVNode 的前一个 vnode,即 prevVNode
                const prevVNode = newChildren[i - 1]
                // 如果 prevVNode 不存在,则说明当前 newVNode 是第一个节点,它不需要移动
                if (prevVNode) {
   
                    // 由于我们要将 newVNode 对应的真实 DOM 移动到prevVNode 所对应真实 DOM 后面,
                    // 所以我们需要获取 prevVNode 所对应真实 DOM 的下一个兄弟节点,并将其作为锚点
                    const anchor = prevVNode.el.nextSibling
                    // 调用 insert 方法将 newVNode 对应的真实 DOM 插入到锚点元素前面,
                    // 也就是 prevVNode 对应真实 DOM 的后面
                    insert(newVNode.el, container, anchor)
                }
            } else {
   
                lastIndex = j
            }
            break

        }
    }
}

注意在mountElement patchElement方法里都处理保存了真实DOM,所以上面才能直接拿到对应的真实DOM.

  //mountElement
  const el = vnode.el = createElement(vnode.type)
  //patchElement
   const el = n2.el = n1.el

接下来考虑一下有新增元素的情况

  1. 想办法找到新增节点
  2. 将新增节点挂载到正确位置

所以整体的代码是

const oldChildren = n1.children const newChildren = n2.children const oldLen = oldChildren.length const newLen = newChildren.length const commonLength = Math.min(oldLen, newLen) let lastIndex = 0
for (let i = 0; i < newLen; i++) {
   
    const newVNode = newChildren[i];
    // 在第一层循环中定义变量 find,代表是否在旧的一组子节点中找到可复用的节点,
    // 初始值为 false,代表没找到
    let find = false
    for (j = 0; j < oldChildren.length; j++) {
   
        const oldVNode = oldChildren[j]
        if (newVNode.key === oldVNode.key) {
   
            // 一旦找到可复用的节点,则将变量 find 的值设为 true
            find = true patch(oldVNode, newVNode, container) if (j < lastIndex) {
   
                const prevVNode = newChildren[i - 1]
                if (prevVNode) {
   
                    const anchor = prevVNode.el.nextSibling insert(newVNode.el, container, anchor)
                }
            } else {
   
                lastIndex = j
            }
            break
        }
    }
    // 如果代码运行到这里,find 仍然为 false,
    // 说明当前 newVNode 没有在旧的一组子节点中找到可复用的节点
    // 也就是说,当前 newVNode 是新增节点,需要挂载
    if (!find) {
   
        // 为了将节点挂载到正确位置,我们需要先获取锚点元素
        // 首先获取当前 newVNode 的前一个 vnode 节点
        const prevVNode = newChildren[i - 1] let anchor = null
        if (prevVNode) {
   
            anchor = prevVNode.el.nextSibling
        } else {
   
            // 如果没有前一个 vnode 节点,说明即将挂载的新节点是第一个子节
            // 这时我们使用容器元素的 firstChild 作为锚点
            anchor = container.firstChild
        }
        // 挂载 newVNode
        patch(null, newVNode, container, anchor)
    }

}

注意最后并不是直接insert插入,而是重新调用patch方法,因为新增节点的type不同处理流程也不同,需要重新调用patch.

所以patch,patch中的mountElement,mountElement中的insert函数需要添加一个锚点参数用来在指定位置添加.

之后当然是考虑元素需要卸载的情况,这种情况其实处理起来很简单.

只需要在双层for循环之后,即更新操作完成.遍历旧数组,查找key在新数组没有对应的元素,将其卸载.

// 上一步的更新操作完成后
// 遍历旧的一组子节点
for (let i = 0; i < oldChildren.length; i++) {
   
    const oldVNode = oldChildren[i]
    // 拿旧子节点 oldVNode 去新的一组子节点中寻找具有相同 key 值的节点
    const has = newChildren.find(vnode = >vnode.key === oldVNode.key) if (!has) {
   
        // 如果没有找到具有相同 key 值的节点,则说明需要删除该节点
        // 调用 unmount 函数将其卸载
        unmount(oldVNode)
    }
}

unmount函数在上节渲染器有提到

     function unmount(vnode) {
   
            if (vnode.type === Fragment) {
   
                vnode.children.forEach(c => unmount(c))
                return
            }
            const parent = vnode.el.parentNode
            if (parent) {
   
                parent.removeChild(vnode.el)
            }
        }

下一节讲双端Diff

相关文章
|
6天前
|
监控 JavaScript 前端开发
Vue 异步渲染
【10月更文挑战第23天】Vue 异步渲染是提高应用性能和用户体验的重要手段。通过理解异步渲染的原理和优化策略,我们可以更好地利用 Vue 的优势,开发出高效、流畅的前端应用。同时,在实际开发中,要注意数据一致性、性能监控和调试等问题,确保应用的稳定性和可靠性。
|
11天前
|
算法 JavaScript UED
Diff 算法的实现原理
【10月更文挑战第18天】Diff 算法是 Vue.js 中实现高效 DOM 更新的核心机制,通过合理的比较和优化策略,能够在保证界面正确性的同时,最大程度地减少 DOM 操作,提高应用的性能和用户体验。
21 2
|
11天前
|
算法 JavaScript
Vue 中的 Diff 算法
【10月更文挑战第18天】需要注意的是,Diff 算法虽然能够提高性能,但在某些复杂的场景下,可能仍然会存在一些性能瓶颈。因此,在实际开发中,我们需要根据具体情况合理地使用 Diff 算法,并结合其他优化手段来提高应用的性能。
7 1
|
19天前
|
JavaScript 算法 前端开发
vue 中diff算法
【10月更文挑战第10天】
24 1
|
29天前
|
JavaScript 算法 前端开发
【VUE】Vue的diff算法和React的diff算法
【VUE】Vue的diff算法和React的diff算法
|
11天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
30天前
|
机器学习/深度学习 算法 数据安全/隐私保护
基于MSER和HOG特征提取的SVM交通标志检测和识别算法matlab仿真
### 算法简介 1. **算法运行效果图预览**:展示算法效果,完整程序运行后无水印。 2. **算法运行软件版本**:Matlab 2017b。 3. **部分核心程序**:完整版代码包含中文注释及操作步骤视频。 4. **算法理论概述**: - **MSER**:用于检测显著区域,提取图像中稳定区域,适用于光照变化下的交通标志检测。 - **HOG特征提取**:通过计算图像小区域的梯度直方图捕捉局部纹理信息,用于物体检测。 - **SVM**:寻找最大化间隔的超平面以分类样本。 整个算法流程图见下图。
|
8天前
|
人工智能 算法 数据安全/隐私保护
基于遗传优化的SVD水印嵌入提取算法matlab仿真
该算法基于遗传优化的SVD水印嵌入与提取技术,通过遗传算法优化水印嵌入参数,提高水印的鲁棒性和隐蔽性。在MATLAB2022a环境下测试,展示了优化前后的性能对比及不同干扰下的水印提取效果。核心程序实现了SVD分解、遗传算法流程及其参数优化,有效提升了水印技术的应用价值。
|
9天前
|
机器学习/深度学习 算法 数据安全/隐私保护
基于贝叶斯优化CNN-LSTM网络的数据分类识别算法matlab仿真
本项目展示了基于贝叶斯优化(BO)的CNN-LSTM网络在数据分类中的应用。通过MATLAB 2022a实现,优化前后效果对比明显。核心代码附带中文注释和操作视频,涵盖BO、CNN、LSTM理论,特别是BO优化CNN-LSTM网络的batchsize和学习率,显著提升模型性能。
|
14天前
|
存储
基于遗传算法的智能天线最佳阵列因子计算matlab仿真
本课题探讨基于遗传算法优化智能天线阵列因子,以提升无线通信系统性能,包括信号质量、干扰抑制及定位精度。通过MATLAB2022a实现的核心程序,展示了遗传算法在寻找最优阵列因子上的应用,显著改善了天线接收功率。