diff算法优化:节点打Tag

vue3.0底层,会自动识别某个节点是否是动态的,如果是动态的会自动生成标识(不同的动态会有不同的标识对应,如内容文本的动态,或者id的动态),从而在每次更新dom时,直接跳过哪些静态的节点,直接定位到动态的节点,大大节省效率。

1
2
3
4
5
<div>
<p>text</p>
<p>string</p>
<p>{{msg}}</p>
</div>

会转化为:

1
2
3
4
5
6
7
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, "text"),
_createVNode("p", null, "string"),
_createVNode("p", null, _toDisplayString(_ctx.msg), 1)
]));
}

由上面代码可以看到前两个p标签和最后一个p标签的区别,最后一个标签1,表示该动态节点是TEXT类型,官方给出的详细说明:github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
export const enum PatchFlags {
// 动态文本节点
TEXT = 1,

// 动态 class
CLASS = 1 << 1, // 2

// 动态 style
STYLE = 1 << 2, // 4

// 动态属性,但不包含类名和样式
// 如果是组件,则可以包含类名和样式
PROPS = 1 << 3, // 8

// 具有动态 key 属性,当 key 改变时,需要进行完整的 diff 比较。
FULL_PROPS = 1 << 4, // 16

// 带有监听事件的节点
HYDRATE_EVENTS = 1 << 5, // 32

// 一个不会改变子节点顺序的 fragment
STABLE_FRAGMENT = 1 << 6, // 64

// 带有 key 属性的 fragment 或部分子字节有 key
KEYED_FRAGMENT = 1 << 7, // 128

// 子节点没有 key 的 fragment
UNKEYED_FRAGMENT = 1 << 8, // 256

// 一个节点只会进行非 props 比较
NEED_PATCH = 1 << 9, // 512

// 动态 slot
DYNAMIC_SLOTS = 1 << 10, // 1024

// 静态节点
HOISTED = -1,

// 指示在 diff 过程应该要退出优化模式
BAIL = -2
}

静态提升 hoistStatic

1
2
3
4
5
6
7
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, "text"),
_createVNode("p", null, "string"),
_createVNode("p", null, _toDisplayString(_ctx.msg), 1)
]));
}

在Vue3中使用了静态提升后,对于不参与更新的元素,只会被创建一次,在渲染时直接复用即可:

1
2
3
4
5
6
7
8
9
const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "text", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "string", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_hoisted_1,
_hoisted_2,
_createVNode("p", null, _toDisplayString(_ctx.msg), 1)
]));
}

事件侦听器缓存 cacheHandlers

默认情况下事件会被视为动态绑定,每次都会追踪它的变化,但是因为是同一个函数,所以将它直接缓存起来复用,就会提升性能:

1
2
3
<div>
<button @click="onclick">点击</div>
</div>

默认会转化为:

1
2
3
4
5
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", { onClick: _ctx.onClick }, "点击", 8 /* PROPS */, ["onClick"]);
]));
}

开启cacheHandlers之后,则会转化成:

1
2
3
4
5
6
7
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
}, "点击", 8 /* PROPS */, ["onClick"]);
]));
}

使用Proxy代替Object.defineProperty()

ES6 原生提供 Proxy 构造函数,MDN上的解释为:Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等),什么意思呢?可以理解为:在对象之前设置一个“拦截”,当该对象被访问的时候,都必须经过这层拦截,这就意味着你可以在这层拦截中,进行各种操作。

1
const o = new Proxy(target, handler);
  • target: 所要拦截的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)

  • handler:一个对象,定义要拦截的行为

vue2.x实现数据响应的原理

递归遍历data中的数据,使用 Object.defineProperty()劫持 getter和setter,在getter中做数据依赖收集处理,在setter中 监听数据的变化,并通知订阅当前数据的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Observer {
constructor(data) {
// 遍历参数data的属性,给添加到this上
for(let key of Object.keys(data)) {
if(typeof data[key] === 'object') {
data[key] = new Observer(data[key]);
}
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
console.log('你访问了' + key);
return data[key]; // 中括号法可以用变量作为属性名,而点方法不可以;
},
set(newVal) {
console.log('你设置了' + key);
console.log('新的' + key + '=' + newVal);
if(newVal === data[key]) {
return;
}
data[key] = newVal;
}
})
}
}
}

const obj = {
name: 'app',
age: '18',
a: {
b: 1,
c: 2,
},
}
const app = new Observer(obj);
app.age = 20;
console.log(app.age);
app.newPropKey = '新属性';
console.log(app.newPropKey);

这样做的问题:

  • 检测不到对象属性的添加和删除:当你在对象上新加了一个属性newProperty,当前新加的这个属性并没有加入vue检测数据更新的机制(因为是在初始化之后添加的)。vue.$set是能让vue知道你添加了属性, 它会给你做处理,$set内部也是通过调用Object.defineProperty()去处理的

  • 无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。

  • 当data中数据比较多且层级很深的时候,会有性能问题,因为要遍历data中所有的数据并给其设置成响应式的。

所以,上面的输出会变成

1
2
3
4
5
6
7
// 修改 obj原有的属性 age的输出
你设置了age
新的age=20
你访问了age
20
// 设置新属性的输出
新属性

vue3.0的实现

因为Proxy是拦截对象,对对象进行一个”拦截”,外界对该对象的访问,都必须先通过这层拦截。无论访问对象的什么属性,之前定义的还是新增的,它都会走到拦截中,而且不需要遍历data了,大大提升了性能。

上面的代码,如果用Proxy实现的话,则会是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const obj = {
name: 'app',
age: '18',
a: {
b: 1,
c: 2,
},
}
const p = new Proxy(obj, {
get(target, propKey, receiver) {
console.log('你访问了' + propKey);
return Reflect.get(target, propKey, receiver);
},
set(target, propKey, value, receiver) {
console.log('你设置了' + propKey);
console.log('新的' + propKey + '=' + value);
Reflect.set(target, propKey, value, receiver);
}
});
p.age = '20';
console.log(p.age);
p.newPropKey = '新属性';
console.log(p.newPropKey);

同理,输出会变成:

1
2
3
4
5
6
7
8
9
10
11
// 修改原对象的age属性
你设置了age
新的age=20
你访问了age
20

// 设置新的属性
你设置了newPropKey
新的newPropKey=新属性
你访问了newPropKey
新属性