简单Diff算法

Diff算法包括简单Diff算法,双端Diff算法,快速Diff算法

Diff算法是什么

操作DOM是非常消耗性能的,比如jQuery的缺点之一就是对DOM频繁的增删改操作修改HTML,会导致页面的性能下降。Vue中虽然也封装的对DOM的操作,但是其修改DOM是通过Diff算法
来完成的。Diff算法通过比较新的虚拟DOM和旧的虚拟DOM判断哪些节点需要添加、哪些需要删除以及哪些需要修改,Vue中Diff算法配合Key尽可能最小量的操作DOM。

虚拟DOM是什么

虚拟DOM其实是一个js对象,通过渲染函数(h函数)产生。
示例:

//一个真实的DOM节点
<a href="https://www.gaojianguo.com">高坚果</a>
//调用h函数
h('a', {props:{href:'https://www.gaojianguo.com'}},'高坚果')
//得到虚拟节点
{"sel":"a", "data":{props: {href:'https://www.gaojianguo.com'}}, "text":"高坚果"}
Key是什么

key相当于一个DOM节点的唯一标识,主要是服务于Diff算法,让Diff算法更加高效。

减小开销
// 旧 vnode
const oldVNode = {
	type: 'div',
	children: [
	{ type: 'p', children: '内容1' },
	{ type: 'p', children: '内容2' },
	]
}

// 新 vnode
const newVNode = {
	type: 'div',
	children: [
	{ type: 'p', children: '内容3' },
	{ type: 'p', children: '内容4' },
	]
}

不用Diff之前的做法,在更新上述节点的时候,需要执行4次操作,也就是卸载所有旧子节点,需要2次DOM操作,挂载所有新子节点,需要2次DOM操作。
通过观察可以发现,新旧虚拟DOM所有子节点都是p标签,只是p标签中的内容发生了变化。其实只需要将旧虚拟DOM中的 '内容1’变为 ‘内容3’‘内容2’ 变为 **‘内容4’**即可,也就是说只需要两次DOM操作就可以完成节点更新。

// 旧 vnode
const oldVNode = {
	type: 'div',
	children: [
	{ type: 'p', children: '内容1' },
	{ type: 'p', children: '内容2' },
	]
}

// 新 vnode
const newVNode = {
	type: 'div',
	children: [
	{ type: 'p', children: '内容2' },
	{ type: 'p', children: '内容1' },
	]
}

上述更新也不需要卸载旧的后挂载新的,只需要移动两个节点的位置即可。

简单Diff算法

怎么判断是否需要移动呢,答案是根据索引进行判断。
在这里插入图片描述
简单Diff算法中采用双层for循环进行遍历,外层遍历新虚拟DOM,内层遍历旧虚拟DOM

第一步:取新的一组子节点中的第一个节点 li,它的 key 为A。尝试在旧的一组子节点中找到具有相同 key 值的可复用节点,发现能够找到,并且该节点在旧的一组子节点中的索引为 0。

第二步:取新的一组子节点中的第二个节点 li,它的 key 为B。尝试在旧的一组子节点中找到具有相同 key 值的可复用节点,发现能够找到,并且该节点在旧的一组子节点中的索引为 1。

第三步:取新的一组子节点中的第三个节点 li,它的 key 为C。尝试在旧的一组子节点中找到具有相同 key 值的可复用节点,发现能够找到,并且该节点在旧的一组子节点中的索引为 2。

在这个过程中,每一次寻找可复用的节点时,都会记录该可复用节点在旧的一组子节点中的位置索引。如果把这些位置索引值按照先后顺序排列,则可以得到一个序列:0、1、2。这是一个递增的序列,在这种情况下不需要移动任何节点。

下面是需要移动的情况
在这里插入图片描述
上图中可以看出,虚拟DOM更新只需要移动B、C节点即可

第一步:取新的一组子节点中的第一个节点 li,它的 key 为A。尝试在旧的一组子节点中找到具有相同 key 值的可复用节点,发现能够找到,并且该节点在旧的一组子节点中的索引为 0。(记录最大索引 --> 0)

第二步:取新的一组子节点中的第二个节点 li,它的 key 为B。尝试在旧的一组子节点中找到具有相同 key 值的可复用节点,发现能够找到,并且该节点在旧的一组子节点中的索引为 2。 (2 > 0,记录最大索引 --> 2)

第三步:取新的一组子节点中的第三个节点 li,它的 key 为C。尝试在旧的一组子节点中找到具有相同 key 值的可复用节点,发现能够找到,并且该节点在旧的一组子节点中的索引为 1。

从第三步可以看出,此时索引的递增顺序已经被打破了(0, 2, 1),则旧DOM的C节点是需要移动的节点,将其移动到新C节点中的前一个节点所对应的旧节点位置的后面,即将其移动到旧B之后

在这里插入图片描述

const oldChildren = n1.children
const newChildren = n2.children
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
    const newVNode = newChildren[i]
    let j = 0
    for (j; j < oldChildren.length; j++) {
        const oldVNode = oldChildren[j]
        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
        }
    }
}

挂载节点

在这里插入图片描述
第一步:取新的一组子节点中第一个节点 C,它的 key 值为C,尝试在旧的一组子节点中寻找可复用的节点。发现能够找到,并且该节点在旧的一组子节点中的索引值为 2。此时,变量lastIndex 的值为 0,索引值 2 不小于 lastIndex 的值 0,所以节点 C 对应的真实 DOM 不需要移动,但是需要将变量
lastIndex 的值更新为 2。

第二步:取新的一组子节点中第二个节点 A,它的 key 值为A,尝试在旧的一组子节点中寻找可复用的节点。发现能够找到,并且该节点在旧的一组子节点中的索引值为 0。此时变量lastIndex 的值为 2,索引值 0 小于 lastIndex 的值 2,所以节点 A 对应的真实 DOM 需要移动,并且应该移动到节点 C
对应的旧 DOM 后面(即旧C节点之后)。

在这里插入图片描述
第三步:取新的一组子节点中第三个节点 D,它的 key 值为D,尝试在旧的一组子节点中寻找可复用的节点。由于在旧的一组子节点中,没有 key 值为 D 的节点,因此渲染器会把节点 D 看作新增节点并挂载它。那么,应该将它挂载到哪里呢?为了搞清楚这个问题,我们需要观察节点 D 在新的一组子节点中的位置。由于节点 D 出现在节点A 后面,所以我们应该把节点D 挂载到节点 A 所对应的虚拟 DOM 后面。
在这里插入图片描述
第四步:取新的一组子节点中第四个节点B,它的 key 值为B,尝试在旧的一组子节点中寻找可复用的节点。发现能够找到,并且该节点在旧的一组子节点中的索引值为 1。此时变量lastIndex 的值为 2,索引值 1 小于 lastIndex 的值 2,所以节点 B 对应的真实 DOM 需要移动,并且应该移动到节点D
对应的虚拟 DOM 后面。
在这里插入图片描述

const oldChildren = n1.children
const newChildren = n2.children
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
    const newVNode = newChildren[i]
    let j = 0
    // 在第一层循环中定义变量 find,代表是否在旧的一组子节点中找到可复用的节点,
    // 初始值为 false,代表没找到
    let find = false
    for (j; 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) {
            // 如果有前一个 vnode 节点,则使用它的下一个兄弟节点作为锚点元素
            anchor = prevVNode.el.nextSibling
        } else {
            // 如果没有前一个 vnode 节点,说明即将挂载的新节点是第一个子节点
            // 这时我们使用容器元素的 firstChild 作为锚点
            anchor = container.firstChild
        }
        // 挂载 newVNode
        patch(null, newVNode, container, anchor)
    }
}

卸载节点
在这里插入图片描述
第一步:取新的一组子节点中的第一个节点 C,它的 key 值为C。尝试在旧的一组子节点中寻找可复用的节点。发现能够找到,并且该节点在旧的一组子节点中的索引值为 2。此时变量lastIndex 的值为 0,索引 2 不小于 lastIndex 的值 0,所以节点 C对应的真实 DOM 不需要移动,但需要更新变量
lastIndex 的值为 2。

第二步:取新的一组子节点中的第二个节点 A,它的 key 值为A。尝试在旧的一组子节点中寻找可复用的节点。发现能够找到,并且该节点在旧的一组子节点中的索引值为 0。此时变量lastIndex 的值为 2,索引 0 小于 lastIndex 的值 2,所以节点 A 对应的真实 DOM 需要移动,并且应该移动到节点C 对
应的虚拟 DOM 后面。
在这里插入图片描述
至此,更新结束。我们发现,节点 B 对应的真实 DOM 仍然存在,所以需要增加额外的逻辑来删除遗留节点。思路很简单,当基本的更新结束时,我们需要遍历旧的一组子节点,然后去新的一组子节点中寻找具有相同 key 值的节点。如果找不到,则说明应该删除该节点。

const oldChildren = n1.children
const newChildren = n2.children
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
    // 省略部分代码
}
// 上一步的更新操作完成后
// 遍历旧的一组子节点
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)
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值