【源码】vue3 速度与体积上的优化

【公司内部分享】探索Vue.js框架内部原理和实现机制,旨在理解其核心概念和技术细节,以便更好地应用和定制Vue.js。

相关文章

vue3 速度与体积上的优化


一、vue3 为什么说渲染、更新更快、内存占用更少

栈调度到协调更新

  • Vue 2 使用栈调度,是一种同步执行更新任务的方式。在栈调度中,更新任务会被依次放入执行栈中,并按照入栈顺序依次执行。只有当一个更新任务执行完毕后,才会执行下一个更新任务。

    • 优点:栈调度的实现简单直观,易于理解和调试。
    • 缺点:栈调度可能会导致长时间的任务阻塞主线程,影响页面的响应速度和流畅性。由于更新任务是同步执行的,因此无法有效地处理优先级较高的任务,可能会导致用户交互不及时。
  • Vue 3 使用协调更新,是一种基于优先级的更新处理方式。在协调更新中,更新任务会根据一定的优先级被调度执行,以确保优先级较高的任务能够得到更及时的响应。协调更新通常采用时间片段调度和任务优先级调度等策略,以提高页面的性能和用户体验。

    • 优点:协调更新能够更灵活地处理更新任务,并根据任务的优先级来决定执行顺序,从而提高页面的响应速度和流畅性。它能够有效地处理优先级较高的任务,如用户交互和动画等。

    • 缺点:协调更新的实现相对复杂,需要考虑更多的因素,如任务优先级、时间片段调度等。由于更新任务是异步执行的,可能会导致一些复杂的同步问题和调试困难。

关于Long Task详细可参考 怎样去优化Long Task

虚拟 DOM 的优化

Vue 3 中对虚拟 DOM 进行了优化,采用了更高效的算法来处理虚拟 DOM 的更新。在 Vue 3 中,虚拟 DOM 的 patching 过程更快,因为它采用了更精细的更新策略,减少了不必要的 DOM 操作。

整体渲染 vs 靶向更新

  • 在 Vue 2 中,当数据发生变化时,整个组件树都会被重新渲染,即使只有部分节点发生了变化。这是因为 Vue 2 的 diff 策略是逐层比较,如果某一层有变化,整个子树都会被重新渲染。
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
42
43
44
45
46
47
48
49
50
51
function patch(oldVnode, vnode) {
// 判断 oldVnode 是否为一个真实的 DOM 元素,如果是,则包装成一个空的虚拟节点
if (!oldVnode) {
createElm(vnode);
} else {
// 判断 oldVnode 和 vnode 是否为相同节点
if (sameVnode(oldVnode, vnode)) {
// 更新相同节点的属性和子节点
patchVnode(oldVnode, vnode);
} else {
// 不是相同节点,则创建新的节点,并替换旧的节点
const parent = oldVnode.parentNode;
createElm(vnode);
parent.replaceChild(vnode.elm, oldVnode.elm);
}
}
}

// 判断两个 vnode 是否为相同节点
function sameVnode(vnode1, vnode2) {
return vnode1.key === vnode2.key && vnode1.tag === vnode2.tag;
}

// 更新相同节点的属性和子节点
function patchVnode(oldVnode, vnode) {
// 更新属性
// ...

// 更新子节点
updateChildren(oldVnode, vnode);
}

// 更新子节点
function updateChildren(oldVnode, vnode) {
// ...
}

// 创建 DOM 元素并挂载到 vnode 上
function createElm(vnode) {
// 创建 DOM 元素
vnode.elm = document.createElement(vnode.tag);

// 更新属性
// ...

// 递归创建子节点
vnode.children.forEach(childVnode => {
createElm(childVnode);
vnode.elm.appendChild(childVnode.elm);
});
}
  • 在 Vue 3 中,通过通过 BlockTree 和 DynamicChild 实现了靶向更新机制,避免了对整个虚拟 DOM 树的全量比较,其中 BlockTree 记录组件的结构和状态,DynamicChild 则追踪动态子节点的信息,只有实际发生变化的节点会被更新,而不必重新渲染整个组件树。
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
const BlockTree = {
// 组件的虚拟 DOM 树
subTree: vnode,
// 组件的容器元素
container: element,
// 组件的父节点
parent: vnode,
// 表示是否是 Fragment 类型
isFragment: false,
// 作用域插槽的 ID 集合
slotScopeIds: new Set(),
// 组件的虚拟 DOM 节点
vnode: vnode,
// 动态子节点的数组
dynamicChildren: [
{
vnode: vnode,
index: 0
},
{
vnode: vnode,
index: 1
}
// 其他动态子节点的信息
]
};

精确度

  • Vue 2 的 diff 策略是基于逐层比较的,因此在某些情况下可能会导致不必要的渲染操作,即使节点的子树没有发生变化。

  • Vue 3 的靶向更新机制能够更精确地定位到需要更新的节点,避免了不必要的比较和渲染操作,从而提高了更新的精确度和效率。

静态节点的提取

组件实例 vs 全局

Vue 3 中通过静态节点的提取,将静态内容从动态内容中分离出来,这样可以避免不必要的重新渲染,提高了渲染的效率。

  • 在 Vue 2 中,整个模板会被编译为 render 函数,并且所有的节点都被认为是动态的,因此每次数据更新时都会重新渲染整个模板。

    1
    with(this){return _c('div',[_c('h1',[_v(_s(title))]),_c('p',[_v("Static Content")]),_c('p',[_v(_s(dynamicContent))])])}

    在这个示例中,<p>Static Content</p> 虽然内容是静态的,但在渲染函数中仍然被视为动态节点,因此每次数据更新时都会重新渲染。

  • 在 Vue 3 中,编译器会识别静态节点并将其提取出来,在渲染函数中静态节点不会被重新渲染。

    1
    2
    3
    4
    5
    6
    7
    const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "Static Content", -1 /* HOISTED */);

    return (openBlock(), createBlock("div", null, [
    createVNode("h1", null, toDisplayString(title), 1 /* TEXT */),
    _hoisted_1,
    createVNode("p", null, toDisplayString(dynamicContent), 1 /* TEXT */)
    ]));

    在这个示例中,<p>Static Content</p> 被识别为静态节点,并被提取到了 _hoisted_1 常量中,在渲染函数中只会被创建一次,而不会被重新渲染。

在 Vue 3 中,不再使用 with 关键字来处理模板编译和渲染。相比之下,Vue 2 在模板编译时会使用 with 关键字来创建一个作用域,使得模板中的变量可以直接访问当前组件实例的数据和方法。然而,在 Vue 3 中,模板编译会将模板转换为 JavaScript 代码,并生成渲染函数。这些渲染函数会明确地使用 JavaScript 的变量引用,而不是依赖于 with 关键字来创建作用域。这样可以避免 with 带来的一些潜在问题和性能开销,并且更容易进行静态分析和优化。

事件侦听器的优化

Vue 3 中对事件侦听器进行了优化,采用了更快的事件绑定和解绑算法,提高了事件处理的效率。

事件委托机制

Vue 3 使用了事件委托机制来处理事件,即将事件监听器绑定到父元素上,通过事件冒泡的方式来触发事件处理函数。这样做的好处是可以减少事件监听器的数量,从而降低内存占用和提高事件处理的效率。

对于普通的事件绑定,例如 @click、@change 等,Vue 3 会将事件监听器绑定到组件的根元素上,然后利用事件冒泡机制来捕获和处理事件。这样可以减少事件监听器的数量,提高性能。而对于自定义事件(通过 this.$emit 触发的事件),事件监听器会绑定在当前组件实例上,而不是根元素上。

事件处理函数的缓存

Vue 3 在渲染过程中会对事件处理函数进行缓存,避免每次渲染时都重新创建事件处理函数。这样可以减少内存分配和回收的开销,提高了事件处理的效率。

  • 编译阶段

    在编译阶段,Vue 3 的编译器会遍历模板,识别事件绑定,并对事件处理函数进行静态分析。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import { createRoot } from 'vue';

    // 编译阶段处理模板
    const template = `
    <div>
    <button @click="handleClick">Click me</button>
    </div>
    `;

    const root = createRoot({
    // 编译模板,静态分析事件处理函数
    template
    });
  • 事件处理函数缓存

    在编译阶段,编译器会对事件处理函数进行缓存,将事件处理函数存储在一个内部的缓存对象中。

    1
    2
    3
    4
    5
    6
    7
    8
    // 定义事件处理函数
    const handleClick = () => {
    console.log('Button clicked!');
    };

    // 将事件处理函数缓存起来
    const eventHandlersCache = new Map();
    eventHandlersCache.set(handleClick, handleClick);
  • 渲染阶段

    在渲染阶段,Vue 3 的运行时会根据编译阶段生成的渲染函数来渲染组件,并在渲染函数中访问事件处理函数的缓存。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 渲染函数中访问事件处理函数缓存
    const render = () => {
    // 根据模板生成渲染函数
    // 在渲染函数中访问事件处理函数的缓存
    const handleClick = eventHandlersCache.get(handleClick);
    // 渲染组件
    return `<button @click="${handleClick}">Click me</button>`;
    };

    // 执行渲染函数
    root.render(render);

组件实例的优化

Proxy 替代 Object.defineProperty

Vue 3 中使用了 Proxy 来实现响应式数据的代理,而不再使用 Vue 2 中的 Object.defineProperty。Proxy 拥有更好的性能,并且在处理大量数据时占用的内存更少。

  • Object.defineProperty 是一个直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象的方法。它可以用来定义属性的 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
    // 创建一个空对象
    const obj = {};

    // 使用 Object.defineProperty 定义一个属性 'name'
    Object.defineProperty(obj, 'name', {
    // 定义 getter 方法,返回属性值
    get() {
    console.log('Getting name');
    return this._name;
    },
    // 定义 setter 方法,设置属性值
    set(value) {
    console.log('Setting name to', value);
    this._name = value;
    },
    // 可以配置属性的可枚举性、可配置性和可写性
    enumerable: true,
    configurable: true
    });

    // 访问属性 'name',会触发 getter 方法
    console.log(obj.name); // 输出:Getting name

    // 设置属性 'name',会触发 setter 方法
    obj.name = 'Alice'; // 输出:Setting name to Alice
  • 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 target = {};

    // 创建一个 Proxy 对象
    const proxy = new Proxy(target, {
    // 拦截属性的读取操作
    get(target, key, receiver) {
    console.log(`Getting ${key}`);
    return target[key];
    },
    // 拦截属性的设置操作
    set(target, key, value, receiver) {
    console.log(`Setting ${key} to`, value);
    target[key] = value;
    return true;
    }
    });

    // 访问属性,会触发 get 方法
    console.log(proxy.name); // 输出:Getting name

    // 设置属性,会触发 set 方法
    proxy.name = 'Bob'; // 输出:Setting name to Bob

组件实例的精简设计

Vue 3 中对组件实例进行了精简设计,去除了一些不常用的属性和方法,从而减少了组件实例的内存占用。

  • 减少不常用的内部属性和方法

    例如一些用于实现响应式系统或者模板编译的内部属性和方法,在 Vue 3 中可能被重新设计或者优化,以减少内存占用。

  • 优化内部数据结构

    Vue 3 可能会对内部的数据结构进行优化,使其更加高效,并且减少不必要的内存占用。

  • 去除不常用的配置项和生命周期钩子

    Vue 3 可能会去除一些不常用的配置项和生命周期钩子,以减少组件实例的复杂度和内存占用。

虚拟 DOM 的优化

Vue 3 中对虚拟 DOM 进行了优化,减少了虚拟 DOM 对象的创建和销毁次数,从而减少了内存占用。此外,Vue 3 中还引入了 Fragment 和 Teleport 等新的虚拟 DOM 类型,进一步减少了虚拟 DOM 对象的内存占用。

  • Fragment

    • 在 Vue 2 中,并没有直接的内置支持 Fragment 的方式,但你可以使用 <template> 标签来达到类似的效果。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <template>
    <div>
    <h1>Hello Vue 2</h1>
    <template>
    <p>This is the first paragraph.</p>
    <p>This is the second paragraph.</p>
    </template>
    </div>
    </template>

    <script>
    export default {
    name: 'App',
    }
    </script>

    • 在 Vue 3 中,Fragment 提供了一种更加清晰、直观、语义化的方式来表示 Fragment,同时具有更好的兼容性和与 JSX 配合更方便的特点。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <template>
    <fragment>
    <h1>Title</h1>
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
    </fragment>
    </template>

    <script>
    import { defineComponent, h } from 'vue';

    export default defineComponent({
    render() {
    return h(Fragment, [
    h('h1', 'Title'),
    h('p', 'Paragraph 1'),
    h('p', 'Paragraph 2')
    ]);
    }
    });
    </script>
  • Teleport

    • 在 Vue 2 中,要实现类似 Teleport 的功能,你可能需要借助于一些第三方库或者手动操作 DOM。通常情况下,你可以使用 JavaScript 来操作 DOM 元素的位置,但这样做可能比较繁琐,并且不够灵活。

    • 在 Vue 3 中,Teleport 允许我们在 DOM 中的任何地方渲染组件的内容,而不受父组件的限制。

      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
      <template>
      <teleport to="body">
      <div class="modal">
      <h2>Modal</h2>
      <p>This modal is teleported to the body element</p>
      </div>
      </teleport>
      </template>

      <script>
      import { defineComponent } from 'vue';

      export default defineComponent({
      // ...
      });
      </script>

      <style>
      .modal {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background-color: white;
      padding: 20px;
      border: 1px solid #ccc;
      }
      </style>

组件的异步加载和懒加载

Vue 3 支持组件的异步加载和懒加载,可以在需要时动态地加载组件,从而减少了页面初始化时的内存占用。

  • Vue 2 中的组件懒加载是通过 webpack 的动态 import 函数来实现的,webpack 会将异步加载的组件分割成单独的代码块,在需要时才动态加载。

    1
    Vue.component('async-component', () => import('./AsyncComponent.vue'));
  • Vue 3 中的组件懒加载是通过返回一个 Promise 来实现的,这个 Promise 在需要时会被解析,并且可以包装成一个工厂函数,返回一个组件定义对象。

    1
    2
    import { defineAsyncComponent } from 'vue';
    const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'));

Vue 3 中的组件异步加载和懒加载不再依赖 webpack 的 import 函数。Vue 3 提供了新的 API defineAsyncComponent 来实现组件的异步加载和懒加载,不再依赖于 webpack 的特定语法或功能。

Tree-Shaking 的支持

Options API 与 Composition API

  • Vue 2 主要使用的是 Options API,即通过一个选项对象来定义组件。这种方式使得组件的逻辑和模板耦合在一起,导致难以识别和移除未使用的代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <template>
    <div>
    <p>{{ message }}</p>
    <button @click="updateMessage">Update Message</button>
    </div>
    </template>

    <script>
    export default {
    data() {
    return {
    message: 'Hello, Vue!'
    };
    },
    methods: {
    updateMessage() {
    this.message = 'Hello, World!';
    }
    }
    };
    </script>
  • Vue 3 引入了 Composition API,使得组件的逻辑可以更灵活地组织和重用,且可以更容易地识别和移除未使用的代码。Composition API 的代码可以更容易地进行 Tree-Shaking。

    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
    <template>
    <div>
    <p>{{ message }}</p>
    <button @click="updateMessage">Update Message</button>
    </div>
    </template>

    <script>
    import { reactive, ref } from 'vue';

    export default {
    setup() {
    const message = ref('Hello, Vue!');

    function updateMessage() {
    message.value = 'Hello, World!';
    }

    return {
    message,
    updateMessage
    };
    }
    };
    </script>

二、vue3 为什么说打包体积更小

模块化设计

Monolithic 架构 与 Modular 架构

  • 在 Vue 2 中,整个框架是以 monolithic 架构为基础构建的。这意味着 Vue 2 的核心功能和特性都打包在一个文件中,开发者一般需要一次性引入完整的 Vue.js 文件,包括模板编译器、响应式系统、虚拟 DOM 等所有功能。

  • Vue 3 采用了更加模块化的设计。它将核心功能拆分为多个独立的模块,开发者可以按需引入需要的功能。这种模块化设计使得 Vue 3 的体积更小、更加灵活。

Options API 与 Composition API

  • Vue 2 主要使用 Options API 来组织组件的逻辑。这种方式下,组件的各种选项(如 data、methods、computed、watch 等)都定义在一个对象中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // main.js
    import Vue from 'vue';

    new Vue({
    el: '#app',
    data: {
    message: 'Hello Vue!'
    },
    template: '<div>{{ message }}</div>'
    });

    在 Vue 2 中,我们通过导入整个 Vue.js 库来使用其全局 API。虽然我们只使用了响应式系统和组件系统,但是整个 Vue.js 库都会被打包到我们的应用程序中。

  • Vue 3 引入了 Composition API,它是一种基于函数的 API,使得组件的逻辑可以更加灵活地组织和复用。开发者可以根据需要组合逻辑,而不再受到 Options API 的限制。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // main.js
    import { createApp, ref } from 'vue';

    const app = createApp({
    template: '<div>{{ message }}</div>'
    });

    const message = ref('Hello Vue!');
    app.provide('message', message);

    app.mount('#app');

    在 Vue 3 中,我们只导入了需要的全局 API,例如 createApp 和 ref。这两个功能分别用于创建 Vue 应用程序实例和定义响应式数据。通过这种方式,我们只会将用到的功能打包到我们的应用程序中,而不会打包整个 Vue.js 库。

ES5 和 CommonJS 与 ES Module

  • Vue 2 默认支持 ES5 和 CommonJS 规范的模块化方式。这使得 Vue 2 可以在浏览器环境和 Node.js 环境中进行使用。

  • Vue 3 默认采用 ES Module 规范,这使得 Vue 3 更加适用于现代的 JavaScript 生态系统。开发者可以使用 import/export 语法来导入和导出 Vue 3 的模块。

Tree-Shaking 支持

Vue 3 对 Tree-Shaking 有更好的支持,可以在打包过程中移除未使用的代码,从而进一步减小最终打包产物的体积。这得益于 Vue 3 的模块化设计和 ES Module 的支持。

逐步渐进式升级

Vue 3 提供了对 Vue 2 的逐步渐进式升级支持。这使得开发者可以在现有项目中逐步引入 Vue 3 的功能,而不需要一次性进行完全的迁移。


【源码】vue3 速度与体积上的优化
https://www.cccccl.com/20240302/源码/vue/vue3 速度与体积上的优化/
作者
Jeffrey
发布于
2024年3月2日
许可协议