前言
最近打算写一个vue3的diff算法的总结,主要包含什么是diff算法,diff算法的流程,为什么要有diff算法。
什么是diff算法
diff算法就是进行虚拟节点对比的一种算法,并返回一个patch对象,用来存储两个节点不同的地方,最后用patch记录的消息去局部更新真实Dom
什么是虚拟dom,什么是真实dom
- 真实dom就是如下这样的html代码在浏览器渲染产生的一个树形结构,它将文档中的每个元素、属性和文本都表示为一个节点,允许开发者通过JavaScript等脚本语言来操作和修改网页的内容和结构,统称document(文档对象模型)
<div> <p>想吃炸鸡</p> </div>
- 虚拟dom是将真实的DOM的数据抽取出来,以对象的形式模拟树形结构,拿刚刚的例子来说它解析出来长这样。
var Vnode = {
tag: 'div',
children: [ { tag: 'p', text: '想吃炸鸡' } ]
}
Vue中通过一个vnode类来实例化出不同类型的虚拟DOM节点,即,每次传入的参数不同即可实例化出不同的节点类型。可以描述出以下几种类型的节点: 源码位置:github.com/vuejs/vue/b…
- 注释节点
- 文本节点
- 元素节点
- 组件节点
- 函数式组件节点
- 克隆节点
为什么要有虚拟dom和diff算法?
首先我们要了解,浏览器内核分为两部分,一个是css渲染器,一个是js引擎,它们都是单线程运行。单线程的优势是开发方便,避免多线程下的死锁、竞争等问题,劣势是失去了并发能力。
浏览器为了避免两个引擎同时修改页面而造成渲染结果不一致的情况,增加了另外一个机制,这两个引擎具有互斥性,也就是说在某个时刻只有一个引擎在运行,另一个引擎会被阻塞。操作系统在进行线程切换的时候需要保存上一个线程执行时的状态信息并读取下一个线程的状态信息,俗称上下文切换(也叫线程切换)。而这个操作相对而言是比较耗时的。
另一个更加耗时的因素是元素及样式变化引起的再次渲染,在渲染过程中最耗时的两个步骤为重排(Reflow)与重绘
重排:修改元素边距、大小,添加、删除元素,改变窗口大小重绘:改变背景颜色、改变字体颜色、改变 visibility 属性值
ps:v-if可以理解成删除添加元素,v-show可以理解成改变visibility属性,相对来说重绘比重排性能更优,所以有的时候为什么推荐使用v-show就是这个原因。
总结:因为频繁操作真实dom会导致重绘和重排耗时,所以我们可以创建虚拟dom,通过diff算法计算出需要更新的dom元素再进行替换。优化dom操作的主要方式
- 在循环外操作元素
- 批量操作元素
- 缓存元素集合
diff算法比较方式是什么样的?
在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较,新Vnode只会和同层旧Vnode比较
而在同层级别的diff比较会进行首尾比较,交叉比较。
diff比较过程
首先呢我们看下图,我们具备一个新Vnode和旧Vnode以及虚拟dom
- 首先说明一下S E代表的是旧的Vnode数组的坐标S代表起始位置0,E代表后置位置2,NS代表新Vnode数组的坐标0,NE代表的后置位置3
- 此时开始第一次比较,新旧的第一个相同,继续下一个,挪动S和NS指针index位置都指向1(S指向B,NS指向C)
- 开始第二次比较发现B和C不相等,于是开始对比尾部的E和NE位置的元素C和D,(E指向C,NE指向D)发现也不相等,结束本次对比
- 开始进入第三次对比,拿NS(指向新Vnode的C)和E(指向旧Vnode的C)进行对比,发现相同,于是真实dom进行更新,把C放到第二位,并且把旧Vnode里面的C置为空,并挪动E指针往前一位(指向B),落地NS指针往后一位(此时指向B)结果如下
- 开始对比S和NS位置的元素,发现相等,于是S指针继续往后挪,NS有人往后挪,最后结果如下
- 此时S比E大说明已经对比完了,结束新旧对比,最后发现NS没有比NE大,说明有新元素。
- 于是拿到新元素加入真实dom的位置,最后结果如下
总结
- 当新or旧Vnode数组起始指针大于尾部指针说明新旧对比完成,结束对比
- 新Vnode数组起始指针没有大于尾部指针,说明有新元素需要添加。
- 旧Vnode数组起始指针没有大于尾部指针,说明有多余的垃圾元素删除不渲染
- 当对比过的(比如红叉C),下一次对比时指针会继续跳过指向后续的元素(
ps这里我偷懒了,画的不够多没有讲出来)
diff算法源码解析-如何比较两个节点是否相同?
//比较两个节点是否相同
function isSameVNodeType(oldNode, newNode) {
// oldNode 和 newNode 节点的 type 和 key 都相同,才是相同节点
return oldNode.type === newNode.type && oldNode.key === newNode.key;
}
patch源码里面干了什么?
const patch = (
n1,//n1表示旧的节点
n2,//n2表示新的节点
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
optimized = false
) => {
// 如果存在旧节点, 且新旧节点类型不同,则销毁旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1);
unmount(n1, parentComponent, parentSuspense, true);
// n1 设置为 null 保证后续都走 mount 逻辑
n1 = null;
}
const { type, shapeFlag } = n2;
//根据元素类型进行不同的处理
switch (type) {
case Text:
// 处理文本节点
processText(n1, n2, container, anchor)
break;
case Comment:
// 处理注释节点
processCommentNode(n1, n2, container, anchor)
break;
case Static:
// 处理静态节点
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break;
case Fragment:
// 处理 Fragment 元素
processFragment(n1, n2, container)
break;
default:
if (shapeFlag & 1 /* ELEMENT */) {
// 处理普通 DOM 元素
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
);
} else if (shapeFlag & 6 /* COMPONENT */) {
// 处理组件
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
);
} else if (shapeFlag & 64 /* TELEPORT */) {
// 处理 TELEPORT
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
)
} else if (shapeFlag & 128 /* SUSPENSE */) {
// 处理 SUSPENSE
;(type as typeof SuspenseImpl).process(n1, n2, container)
}
}
};
ps:在后面的代码我没看了,有时间再补充
vue3和vue2的diff算法区别
Vue2使用的是基于递归的双指针的diff算法,而Vue3使用的是基于数组的动态规划的diff算法, Vue 3的算法效率更高,因为它使用了一些优化技巧,例如按需更新、静态标记等。- Vue3的diff算法相比Vue2更加高效,并且
新增的静态提升优化方式可以进一步提升渲染性能。 在计算key值不同时,Vue2会采用首尾两端比较的方法,而Vue3则采用了更高效的“Map”数据结构。 在节点移动时,Vue2通过splice函数进行数组操作,而Vue3则采用了更轻量级的移动节点算法。 Vue2使用双向指针来进行虚拟DOM的比较,而Vue3则使用了单向链表的方式。 Vue3.x 的 diff 算法在核心思路上都是基于同层比较和节点复用的策略,但Vue3.x在实现细节和性能优化上做了更多的工作,使得其渲染性能更加出色。 在实际使用中,Vue3.x 的性能提升对于大型应用或需要频繁更新 DOM 的场景尤为明显。
总结
vue3通过静态标记,同层比较复用策略,基于数组的移动节点算法等方式改善了diff算法的性能。