Vue3中的diff算法优化
前言
之前的文章中已经讲到vue2中的diff算法主要包括五种对比方式,是针对于vue2中数组更新的七种方式而定的,但是vue3中支持数组更新的方式是包括数组原型上的所有方法,同时vue3diff算法的优化不仅仅解决更多的适配方法,同时也是更迅速的进行组件的更新。
优化方向
1. 节点更新,添加静态标记
2. 数组循环的优化
3. 事件优化
节点更新优化
我们编写的组件有些是动态的数据,而有些则是静态的数据。 所以在vue3中对于动态的数据进行标记的添加,对于静态的数据则是直接利用缓存
举个例子:
<div>
<span>hello</span>
<span>{{state}}</span>
</div>
其中hello是静态的数据可以进行缓存使用,不需要在下次更新时进行比对,而state是动态数据我们需要给它打上标记,以便下次进行更新
静态标记如下:
vue2中的vnode:
vue3中的Vnode:
vue3中标记类型有两种方式:
一种是ShapeFlags ,一种是# patchFlags
shapeFLag
属性使用二进制的方式描述了组件的类型。
export ShapeFlags ={
ELEMENT = 1, // 普通的HTML元素
FUNCTIONAL_COMPONENT = 1 << 1, // 函数式组件
STATEFUL_COMPONENT = 1 << 2, // 有状态组件
TEXT_CHILDREN = 1 << 3, // 子节点是文本
ARRAY_CHILDREN = 1 << 4, // 子节点是数组
SLOTS_CHILDREN = 1 << 5, // 子节点是插槽
TELEPORT = 1 << 6, // 当前是个teleport组件
SUSPENSE = 1 << 7, // 当前是个suspense组件
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 表示需要被keep-live的有状态组件
COMPONENT_KEPT_ALIVE = 1 << 9, // 已经被keep-live的有状态组件
}
patchFlag是在编译模板时,给vnode添加的一个标识信息,这个标识信息反映了vnode
的哪些部位绑定了动态值,这样在runtime
阶段,可以根据patchFlag
判断出哪些内容需要更新,实现定点更新。
export PatchFlags = {
// 表示vnode具有动态textContent的元素
TEXT = 1,
// 表示vnode具有动态的class
CLASS = 1 << 1,
// 表示具有动态的style
STYLE = 1 << 2,
// 表示具有动态的非class和style的props
PROPS = 1 << 3,
// 表示props具有动态的key,与CLASS、STYLE、PROPS冲突
FULL_PROPS = 1 << 4,
// 表示有监听事件(在同构期间需要添加)
HYDRATE_EVENTS = 1 << 5,
// 表示vnode是个children顺序不会改变的fragment
STABLE_FRAGMENT = 1 << 6,
// 表示children带有key的fragment
KEYED_FRAGMENT = 1 << 7,
// 表示children没有key的fragment
UNKEYED_FRAGMENT = 1 << 8,
// 表示vnode只需要非props的patch。例如只有标签中只有ref或指令
NEED_PATCH = 1 << 9,
// 表示vnode存在动态的插槽。例如动态的插槽名
DYNAMIC_SLOTS = 1 << 10,
// 表示用户在模板的根级别存在注释而创建的片段,这是一个仅用于开发的标志,因为注释在生产中被剥离
DEV_ROOT_FRAGMENT = 1 << 11,
// 以下都是一些特殊的flag,它们不能使用位运算进行匹配
// 表示vnode经过静态提升
HOISTED = -1,
// diff算法应该退出优化模式
BAIL = -2
}
在编译阶段通过不同的patchflg值进行不同的方法比较
const patchElement = (n1, n2, parentComponent, parentSuspense, isSVG, optimized) => {
const el = (n2.el = n1.el);
let { patchFlag, dynamicChildren, dirs } = n2;
const oldProps = (n1 && n1.props) || EMPTY_OBJ;
const newProps = n2.props || EMPTY_OBJ;
let vnodeHook;
if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parentComponent, n2, n1);
}
if (dirs) {
invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate');
}
if (__HMR__ && parentComponent && parentComponent.renderUpdated) {
// HMR updated, force full diff
patchFlag = 0;
optimized = false;
dynamicChildren = null;
}
if (patchFlag > 0) {
// the presence of a patchFlag means this element's render code was
// generated by the compiler and can take the fast path.
// in this path old node and new node are guaranteed to have the same shape
// (i.e. at the exact same position in the source template)
if (patchFlag & 16 /* FULL_PROPS */) {
// element props contain dynamic keys, full diff needed
patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG);
}
else {
// class
// this flag is matched when the element has dynamic class bindings.
if (patchFlag & 2 /* CLASS */) {
if (oldProps.class !== newProps.class) {
hostPatchProp(el, 'class', null, newProps.class, isSVG);
}
}
// style
// this flag is matched when the element has dynamic style bindings
if (patchFlag & 4 /* STYLE */) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG);
}
// props
// This flag is matched when the element has dynamic prop/attr bindings
// other than class and style. The keys of dynamic prop/attrs are saved for
// faster iteration.
// Note dynamic keys like :[foo]="bar" will cause this optimization to
// bail out and go through a full diff because we need to unset the old key
if (patchFlag & 8 /* PROPS */) {
// if the flag is present then dynamicProps must be non-null
const propsToUpdate = n2.dynamicProps;
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i];
const prev = oldProps[key];
const next = newProps[key];
if (prev !== next) {
hostPatchProp(el, key, prev, next, isSVG, n1.children, parentComponent, parentSuspense, unmountChildren);
}
}
}
}
// text
// This flag is matched when the element has only dynamic text children.
if (patchFlag & 1 /* TEXT */) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children);
}
}
}
};
数组循环优化
相对于vue2中for循环的对比,vue3中你的对比则是比较简单的,vue3中做法如下:
- 从前往后对比 (头部与头部比较)
- 从后往前对比 (尾部与尾部比较)
- 基于最长子序列的比较进行-》移动|添加|删除
举个例子:
旧数组:【a,b,c,d,e,f,g】
新数组:【a, b, f, c, d, e, h, g]
首先是开始最简单的比较获取之前的缓存数据:
- 首先是头头对比,发现不同就停止本次循环【a,b】
- 然后是尾尾比较得到【g】;
经过1.2的比较后得到不同局部数据【f,c,d,e,h】,接下来就是不同数据的比较,这一步就是要使用最长子序列方法进行(移动|添加|删除)的操作。
在源码中使用map函数对数组的值和key进行一个绑定。(有兴趣的同学可以看下源码)
源码中通过 newIndexToOldIndexMap 拿到在数组里对应的下标,生成数组 [ 5, 2, 3, 4, -1 ],-1 是老数组里没有的,没有的数据我们只能往旧节点补充数据。
很明显可以发现在[ 5, 2, 3, 4, -1 ]中【2,3,4】是有序的,并且是依次递增的,此时他对应的节点是【c,d,e】,这个时候我们就很容易的保持【2,3,4】不变进行其他数据的移位,既是在此基础的前方插上一位就能完成新节点的更新
事件优化
举例点击事件:
<div>
<button @click="change">hello</button>
</div>
如果开启了缓存处理,事件的变化不会引起重新渲染。
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("span", {
onClick: _cache[1] || (_cache[1] = $event => (_ctx.onClick($event)))
}, "hello")
]))
}
转载自:https://juejin.cn/post/7159488062689378318