# Vue3是如何变快的

# 1.diff算法优化

vue2中虚拟dom是全量对比

vue3新增了静态标记(PatchFlag)

  • 在与上次虚拟节点进行对比时,只对比带有patch flag的节点
  • 并且可以通过flag的信息得知当前节点要对比的具体内容
  • 在创建虚拟dom时,会根据dom中的内容添加静态标记
<div>
  <p>我是段落</p>
  <p>{{msg}}</p>
</div>
1
2
3
4
import {
  createVNode as _createVNode,
  toDisplayString as _toDisplayString,
  openBlock as _openBlock,
  createBlock as _createBlock
} from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("p", null, "我是段落"),
    _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# PatchFlags

PatchFlags 是 Vue.js 内部用于表示虚拟 DOM 节点更新类型的一种标志位

export const enum PatchFlags {
  TEXT = 1, // 动态文本节点
  CLASS = 1 << 1, // 2 动态class
  STYLE = 1 << 2, // 4 动态style
  PROPS = 1 << 3, // 8 动态属性,但不包含类名和样式
  FULL_PROPS = 1 << 4, // 16 具有动态 key 属性,当 key 改变时,需要进行完整的 diff 比较
  HYDRATE_EVENTS = 1 << 5, // 32 带有监听事件的节点
  STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的 fragment
  KEYED_FRAGMENT = 1 << 7, // 128 带有key属性的 fragment 或部分带有 key
  UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 fragment
  NEED_PATCH = 1 << 9, // 512 一个节点只会进行非 props 比较
  DYNAMIC_SLOTS = 1 << 10, // 1024
  HOISTED = -1,
  BAIL = -2
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.静态提升hoistStatic

vue2会比较每一个Vnode,这样对于一些不参与更新的元素,会浪费性能

Vue3diff算法在初始化时会给每一个虚拟节点一个patchFlag,并且只会比较patchFlag发生变化的Vnode,从而节省性能开销

看以下一段vue代码

<div>
  {{msg}}
  <span>123</span>
  <button @click="onClick">按钮</button>
</div>
1
2
3
4
5
  • 无静态提升
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createTextVNode(_toDisplayString(_ctx.msg) + " ", 1 /* TEXT */),
    _createElementVNode("span", null, "123"),
    _createElementVNode("button", { onClick: _ctx.onClick }, "按钮", 8 /* PROPS */, ["onClick"])
  ]))
}
1
2
3
4
5
6
7
  • 有静态提升
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "123", -1 /* HOISTED */)
const _hoisted_2 = ["onClick"]

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createTextVNode(_toDisplayString(_ctx.msg) + " ", 1 /* TEXT */),
    _hoisted_1,
    _createElementVNode("button", { onClick: _ctx.onClick }, "按钮", 8 /* PROPS */, _hoisted_2)
  ]))
}
1
2
3
4
5
6
7
8
9
10

# 3.事件侦听缓存cacheHandlers

<div>
  <button @click="onClick">按钮</button>
</div>
1
2
3
  • 开启事件侦听缓存之前
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("button", { onClick: _ctx.onClick }, "按钮", 8 /* PROPS */, ["onClick"])
  ]))
}
1
2
3
4
5
  • 开启cacheHandlers 之后
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("button", {
      onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.onClick && _ctx.onClick(...args)))
    }, "按钮")
  ]))
}
1
2
3
4
5
6
7

事件侦听缓存本质上是去除了不必要的diff比较

cacheHandlers 在 Vue 3 中用于提高性能的方式是通过缓存组件中的事件处理程序(methods)以减少内存占用和函数创建的开销。

  1. 减少内存占用: 在默认情况下,每次组件重新渲染时,事件处理程序都会创建新的函数实例。这会导致内存占用的增加,因为每个函数都需要存储在内存中。通过缓存处理程序,Vue 3 可以重用相同的函数实例,减少了内存占用。
  2. 减少函数创建开销: 创建函数实例是一个开销较大的操作,特别是在组件频繁重新渲染时。通过缓存事件处理程序,Vue 3 可以避免在每次重新渲染时都创建新的函数实例,从而减少了性能开销。
  3. 一致性: 缓存事件处理程序可以确保在组件的生命周期内使用相同的函数实例,从而确保事件处理程序的一致性。这对于涉及到事件监听和取消监听的场景非常重要,因为你始终在操作相同的函数。

----------------------------------------------------------

==cacheHandlers是如何缓存事件处理程序的==

  1. 首次创建事件处理程序: 当组件首次创建时,事件处理程序的函数将正常创建,就像通常情况下一样。这些函数会被存储在组件实例内。
  2. 缓存事件处理程序: 一旦事件处理程序被创建,Vue 3 会将它们的函数引用缓存起来,以便在未来的渲染周期中可以重复使用相同的函数实例。
  3. 重复使用函数实例: 在组件的后续渲染中,Vue 3 将重复使用已缓存的函数实例,而不是每次都创建新的函数。这减少了内存占用和函数创建的开销。

----------------------------------------------------------

==将事件处理函数缓存起来,不是会额外消耗内存吗,为什么还能够减少内存占用呢==

  1. 减少内存占用: 当事件处理程序不被缓存时,每次组件重新渲染时,都会创建新的函数实例,并且这些新函数实例需要存储在内存中。如果组件频繁重新渲染,这会导致内存占用的增加,因为大量函数实例被创建并保留在内存中。
  2. 权衡: 缓存事件处理程序后,Vue 3 可以在重新渲染时重复使用相同的函数实例,而不是每次都创建新的函数。这减少了新函数实例的创建,因此减少了内存占用。这是因为函数实例是对象,它们需要占用一定的内存。
  3. 内存回收: 此外,由于使用相同的函数实例,垃圾收集器(garbage collector)可以更容易地回收未使用的函数实例,从而帮助减少内存占用。

虽然缓存事件处理程序可能需要一些额外的内存来存储函数引用,但这通常比在每次重新渲染时创建新的函数实例更节省内存。这是一种优化,特别适用于需要频繁重新渲染的组件,因为它可以减少内存分配和回收的次数。

----------------------------------------------------------

==从代码层面上实现==

// 简化的 Vue 3 内部实现
const cache = new WeakMap();

function createHandlerProxy(fn) {
  // 检查缓存中是否已存在该处理程序的代理
  if (cache.has(fn)) {
    return cache.get(fn);
  }

  // 创建代理处理程序
  const proxy = new Proxy(fn, {
    apply(target, thisArg, args) {
      // 调用原始处理程序
      return Reflect.apply(target, thisArg, args);
    },
  });

  // 将代理存储在缓存中
  cache.set(fn, proxy);

  return proxy;
}

// 示例组件
const component = {
  setup() {
    // 创建缓存处理程序
    const cachedHandler = createHandlerProxy(() => {
      console.log('Event handler is called.');
    });

    return {
      handleClick: cachedHandler, // 使用缓存的处理程序
    };
  },
};
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

# 性能优势

Vue3比Vue2性能好的其中一点就是shallowRef的使用

Vue 2 的响应式系统依赖于 Object.defineProperty,并且对于对象的每个属性都会进行深层递归的响应式处理。