图解双端diff
# 图解双端diff
面试题:说一下 Vue3 中的 diff 相较于 Vue2 有什么变化?
- Vue2: 双端diff
- Vue3: 快速diff
1. diff的概念
diff 算法是用于比较两棵虚拟 DOM 树的算法,目的是找到它们之间的差异,并根据这些差异高效地更新真实 DOM,从而保证页面在数据变化时只进行最小程度的 DOM 操作。
思考🤔:为什么需要进行diff,不是已经有响应式了么?
答案:响应式虽然能够侦测到响应式数据的变化,但是只能定位到组件,代表着某一个组件要重新渲染。组件的重新渲染就是重新执行对应的渲染函数,此时就会生成新的虚拟 DOM 树。但是此时我们并不知道新树和旧树具体哪一个节点有区别,这个时候就需要diff算法来找到两棵树的区别。
2. diff算法的特点
- 分层对比:它会逐层对比每个节点和它的子节点,避免全树对比,从而提高效率。
- 相同层级节点对比:在进行 diff 对比的时候,Vue会假设对比的节点是同层级的,也就是说,不会做跨层的比较。
3. diff算法详细流程
从根节点开始比较,看是否相同。所谓相同,是指两个虚拟节点的标签类型、key 值均相同,但 input 元素还要看 type 属性
- 相同
- 相同就说明能够复用,此时就会将旧虚拟DOM节点对应的真实DOM赋值给新虚拟DOM节点
- 对比新节点和旧节点的属性,如果属性有变化更新到真实DOM. 这说明了即便是对 DOM 进行复用,也不是完全不处理,还是会有一些针对属性变化的处理
- 进入【对比子节点】
- 不相同
- 如果不同,该节点以及往下的子节点没有意义了,全部卸载
- 直接根据新虚拟DOM节点递归创建真实DOM,同时挂载到新虚拟DOM节点
- 销毁旧虚拟DOM对应的真实DOM,背后调用的是 vnode.elm.remove( ) 方法
- 如果不同,该节点以及往下的子节点没有意义了,全部卸载
- 相同
对比子节点:
- 仍然是同层做对比
- 深度优先
- 同层比较时采用的是双端对比
4. 双端对比
之所以被称之为双端,是因为有两个指针,一个指向头节点,另一个指向尾节点,如下所示:
无论是旧的虚拟 DOM 列表,还是新的虚拟 DOM 列表,都是一头一尾两个指针。
接下来进入比较环节,整体的流程为:
步骤一:新头和旧头比较
相同:
复用 DOM 节点
新旧头索引自增
重新开始步骤一
不相同:进入步骤二
步骤二:新尾和旧尾比较
相同:
复用 DOM 节点
新旧尾索引自减
重新开始步骤一
不相同,进入步骤三
步骤三:旧头和新尾比较
相同:
说明可以复用,并且说明节点从头部移动到了尾部,涉及到移动操作,需要将旧头对应的 DOM 节点移动到旧尾对应的 DOM 节点之后
旧头索引自增,新尾索引自减
重新开始步骤一
不相同,进入步骤四
步骤四:新头和旧尾比较
相同:
说明可以复用,并且说明节点从尾部移动到了头部,仍然涉及到移动操作,需要将旧尾对应的 DOM 元素移动到旧头对应的 DOM 节点之前
新头索引自增,旧尾索引自减
重新开始步骤一
不相同:进入步骤五
暴力比较:上面 4 个步骤都没找到相同的,则采取暴力比较。在旧节点列表中寻找是否有和新节点相同的节点,
找到
说明是一个需要移动的节点,将其对应的 DOM 节点移动到旧头对应的 DOM 节点之前
新头索引自增
回到步骤一
没找到
说明是一个新的节点,创建新的 DOM 节点,插入到旧头对应的 DOM 节点之前
新头索引自增
回到步骤一
新旧节点列表任意一个遍历结束,也就是 oldStart > OldEnd 或者 newStart > newEnd 的时候,diff 比较结束。
- 旧节点列表有剩余(newStart > newEnd):对应的旧 DOM 节点全部删除掉
- 新节点列表有剩余(oldStart > OldEnd):将新节点列表中剩余的节点创建对应的 DOM,放置于新头节点对应的 DOM 节点后面
综合示例
当前旧 Vnode 和新 VNode 如下图所示:
头头对比,能够复用,新旧头指针右移
头头不同,尾尾相同,能够复用,尾尾指针左移
头头不同,尾尾不同,旧头新尾相同,旧头对应的真实DOM移动到旧尾对应的真实DOM之后,旧头索引自增,新尾索引自减
头头不同,尾尾不同,旧头新尾不同,新头旧尾相同,旧尾对应的真实DOM移动到旧头对应的真实DOM之前,新头索引自增,旧尾索引自减
头头不同,尾尾不同,旧头新尾不同,新头旧尾不同,进入暴力对比,找到对应节点,将对应的真实DOM移动到旧头对应的真实DOM之间,新头索引自增
头头不同,尾尾不同,旧头新尾不同,新头旧尾相同,将旧尾对应的真实DOM移动到旧头对应的真实DOM之前,新头索引自增,旧尾索引自减
头头不同,尾尾不同,旧头新尾不同,新头旧尾不同,暴力对比发现也没找到,说明是一个全新的节点,创建新的DOM节点,插入到旧头对应的DOM节点之前,新头索引自增
newEnd > newStart,diff 比对结束,旧 VNode 列表还有剩余,直接删除即可。
-EOF-