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

相关文章
|
JavaScript 前端开发 算法
vue渲染页面的原理
vue渲染页面的原理
404 56
|
JavaScript 算法 前端开发
vue 中diff算法
【10月更文挑战第10天】
430 137
|
算法 JavaScript
Vue 中的 Diff 算法
【10月更文挑战第18天】需要注意的是,Diff 算法虽然能够提高性能,但在某些复杂的场景下,可能仍然会存在一些性能瓶颈。因此,在实际开发中,我们需要根据具体情况合理地使用 Diff 算法,并结合其他优化手段来提高应用的性能。
290 56
|
敏捷开发 人工智能 JavaScript
Figma-Low-Code:快速将Figma设计转换为Vue.js应用,支持低代码渲染、数据绑定
Figma-Low-Code 是一个开源项目,能够直接将 Figma 设计转换为 Vue.js 应用程序,减少设计师与开发者之间的交接时间,支持低代码渲染和数据绑定。
1645 3
Figma-Low-Code:快速将Figma设计转换为Vue.js应用,支持低代码渲染、数据绑定
|
监控 JavaScript 前端开发
Vue 异步渲染
【10月更文挑战第23天】Vue 异步渲染是提高应用性能和用户体验的重要手段。通过理解异步渲染的原理和优化策略,我们可以更好地利用 Vue 的优势,开发出高效、流畅的前端应用。同时,在实际开发中,要注意数据一致性、性能监控和调试等问题,确保应用的稳定性和可靠性。
|
算法 JavaScript UED
Diff 算法的实现原理
【10月更文挑战第18天】Diff 算法是 Vue.js 中实现高效 DOM 更新的核心机制,通过合理的比较和优化策略,能够在保证界面正确性的同时,最大程度地减少 DOM 操作,提高应用的性能和用户体验。
597 2
|
JavaScript 算法 前端开发
Vue diff 算法探秘:如何实现快速渲染
Vue diff 算法探秘:如何实现快速渲染
Vue diff 算法探秘:如何实现快速渲染
|
JavaScript 算法 前端开发
vue 中diff算法
vue 中diff算法
198 0
|
算法 JavaScript
vue的diff算法?
vue的diff算法?
252 0
|
JavaScript 算法 前端开发
"揭秘Vue.js的高效渲染秘诀:深度解析Diff算法如何让前端开发快人一步"
【8月更文挑战第20天】Vue.js是一款备受欢迎的前端框架,以其声明式的响应式数据绑定和组件化开发著称。在Vue中,Diff算法是核心之一,它高效计算虚拟DOM更新时所需的最小实际DOM变更,确保界面快速准确更新。算法通过比较新旧虚拟DOM树的同层级节点,递归检查子节点,并利用`key`属性优化列表更新。虽然存在局限性,如难以处理跨层级节点移动,但Diff算法仍是Vue高效更新机制的关键,帮助开发者构建高性能Web应用。
435 1