5分钟了解Vue3 的新特性

1,926 阅读5分钟

Vue.js 从 1.x 到 2.0 版本,最大的升级就是引入了虚拟 DOM 的概念,它为后续做服务端渲染以及跨端框架 Weex 提供了基础。本文主要讲的是vue2到vue3.0的优化点。

vue2.x的痛点

  • 源码自身的维护性;
  • 数据量大后带来的渲染和更新的性能问题;
  • 一些想舍弃但为了兼容一直保留的鸡肋 API 等;
  • TypeScript 支持;

优化点

一、使用 monorepo管理源码

monorepo 把这些模块拆分到不同的 package 中,每个 package 有各自的 API、类型定义和测试。这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性。

源码目录

vue-next
├─scripts
|    ├─build.js
|    ├─dev.js
|    ├─utils.js
├─packages
|    ├─global.d.ts
|    ├─vue
|    ├─template-explorer
|    ├─size-check
|    ├─shared
|    ├─server-renderer
|    ├─runtime-dom
|    ├─runtime-core
|    ├─reactivity
|    ├─compiler-ssr
|    ├─compiler-sfc
|    ├─compiler-dom
|    ├─compiler-core
├─test-dts
  • reactivity:响应式系统
  • runtime-core:与平台无关的运行时核心 (可以创建针对特定平台的运行时 - 自定义渲染器)
  • runtime-dom: 针对浏览器的运行时。包括DOM API,属性,事件处理等
  • runtime-test:用于测试
  • server-renderer:用于服务器端渲染
  • compiler-core:与平台无关的编译器核心
  • compiler-dom: 针对浏览器的编译模块
  • compiler-ssr: 针对服务端渲染的编译模块
  • template-explorer:用于调试编译器输出的开发工具shared:多个包之间共享的内容vue:完整版本,包括运行时和编译器

二、使用 TypeScript 开发源码

从vue2中使用的Flow换成了Typescript

三、性能优化

1. 源码体积优化

  • 首先,移除一些冷门的 feature(比如 filter、inline-template 等);
  • 其次,引入 tree-shaking 的技术,减少打包体积。

tree-shaking 依赖 ES2015 模块语法的静态结构(即 import 和 export),通过编译阶段的静态分析,找到没有引入的模块并打上标记。然后压缩阶段会利用例如 uglify-js、terser 等压缩工具真正地删除这些没有用到的代码。

例如,如果你在项目中没有引入 Transition、KeepAlive 等组件,那么它们对应的代码就不会打包,这样也就间接达到了减少项目引入的 Vue.js 包体积的目的。

2. 数据劫持优化Proxy

Object.defineProperty 切换成 es6的Proxy实现

Proxy 劫持了我们对 observed 对象的一些操作,比如:

  • 访问对象属性会触发 get 函数;
  • 设置对象属性会触发 set 函数;
  • 删除对象属性会触发 deleteProperty 函数;
  • in 操作符会触发 has 函数;
  • 通过 Object.getOwnPropertyNames 访问对象属性名会触发 ownKeys 函数。

Proxy 劫持的是对象本身,并不能劫持子对象的变化,这点和 Object.defineProperty API 一致。但是 Object.defineProperty 是在初始化阶段,即定义劫持对象的时候就已经递归执行了,而 Proxy 是在对象属性被访问的时候才递归执行下一步 reactive,这其实是一种延时定义子对象响应式的实现,在性能上会有较大的提升。

3. 编译优化

  • Vue.js 2.x 的数据更新并触发重新渲染的粒度是组件级的,虽然 Vue 能保证触发更新的组件最小化,但在单个组件内部依然需要遍历该组件的整个 vnode 树。

  • vue3做到动静分离,执行diff时仅对比动态节点。

  • 除此之外,Vue.js 3.0 在编译阶段还包含了对 Slot 的编译优化、事件侦听函数的缓存优化,并且在运行时重写了 diff 算法。

4. diff算法优化

深度递归遍历vnode树,节点的标签和key相同认为是同一个节点则更新,不同则删除,然后处理子节点。

子节点分这几种情况处理:纯文本、vnode 数组和空

  1. 空往往意味着添加或删除;
  2. 纯文本相同直接更新innerText,不同则删除;
  3. 新旧子节点都是vnode数组则diff算法来处理;

vue3.0 diff算法思想

  1. 编译模版时进行静态分析标记动态节点,diff对比差异时仅对比动态节点(性能提升明显);
  2. diff算法先去头去尾,借此缩短遍历对比数组长度(对数组插入和删除操作性能优化明显);
  3. 通过对更新前后子节点数组建立映射表的方式,将O(n^2)复杂度的遍历降低到O(n);
  4. 通过最长递增子序列方法了来diff前后的子节点数组,减少移动操作的次数;
  • 最长递增子序列算法实现:
/*
 * 寻找最长递增子序列
 * 使用动态规划思想,a -> c = a -> b + b -> c
 * 其中p数组存储的是从p[p[i]] 到 p[i] 的最长递增子序列索引,也就是前一个b的索引;
 * r数组存储最后一个元素也就是c的索引
 */
 function getSequenceOfLIS(arr) {
    const p = [0];
    const result = [0];
    for (let i = 0; i < arr.length; i ++) {
        const val = arr[i];
        const top = result[result.length - 1];
        if (arr[top] < val) {
            p[i] = top;
            result.push(i);
            continue;
        }
        // 二分法搜索
        let l = 0, r = result.length - 1;
        while(l < r) {
            const c = (l + r) >> 1;
            if (arr[result[c]] < val) {
                l = c + 1;
            } else {
                r = c;
            }
        }
        if (val < arr[result[l]]) {
            if (l > 0) {
                p[i] = result[l - 1]
            }
            result[l] = i;
        }
    }
    // 回朔p数组,找出最长递增子序列
    let preIndex = result[result.length - 1];
    for (let i = result.length - 1; i > 0; i --) {
        result[i] = preIndex;
        preIndex = p[preIndex]
    }
    return result;
}

四、语法 API 优化:Composition API

1. 优化逻辑组织

编写组件本质就是在编写一个“包含了描述组件选项的对象”,我们把它称为 Options API,符合直觉思维。

Options API 的设计是按照 methods、computed、data、props 这些不同的选项分类,当组件小的时候,这种分类方式一目了然;但是在大型组件中,一个组件可能有多个逻辑关注点,当使用 Options API 的时候,每一个关注点都有自己的 Options,如果需要修改一个逻辑点关注点,就需要在单个文件中不断上下切换和寻找。

Composition API将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去。

2. 优化逻辑复用

  • vue2 有mixin 变量命名容易冲突的问题
  • Composition API 显示的将变量引入当前组件,解决明明冲突问题
  • 简单组件使用Options API,复杂组件使用 Composition API

建议

如果你的项目非常庞大且已经相对稳定,没有什么特别的痛点,那么升级要慎重。