原稿:语雀 · Vue3.0 新特性 · 原目录:Vue › Vue 知识点 › Vue3.0 新特性

参考链接:

vue3.0六大亮点

  • Performance:通过 Proxy 实现双向响应式绑定,相比 defineProperty 的遍历属性的方式效率更高,性能更好,另外 Virtual DOM 更新只 diff 动态部分、事件缓存等,也带来了性能上的提升
  • Tree-Shaking:相比2.x导入整个Vue对象,3.x支持按需导入,只打包需要的代码
  • Composition API:组合式 API,面向函数编程
  • Fragment、Teleport、Suspense:Teleport 即 Protal 传送门,“悬念”,参考了React的设计
  • Better Typescript support:2.x设计之初没有考虑到类型推导,导致适配 ts 比较困难,3.x移除了 this 对象,利用了天然对类型友好的普通变量与函数,对 TypeScript 支持更好
  • Custom Render API:提供了自定义渲染API

性能

Vue3.0 响应式改进

vue3.0 使用 proxy 实现响应式,代替了原来的 Object.defineProperty

  • 将对象作为数据传递给组件实例时,Vue 会将其转换为 proxy。这个 proxy 使 Vue 能够在 property 被访问或修改时执行依赖项跟踪和更改通知。每个 property 都被视为一个依赖项。
  • 首次渲染后,组件将跟踪一组依赖列表——即在渲染过程中被访问的 property。反过来,组件就成为了其每个 property 的订阅者。当 proxy 拦截到 set 操作时,该 property 将通知其所有订阅的组件重新渲染。
const dinner = {
  meal: 'tacos'
}

const handler = {
  get(target, prop, receiver) {
    // track()用于在值变化时跟踪变化(依赖跟踪)
    track(target, prop)
    return Reflect.get(...arguments)
  },
  set(target, key, value, receiver) {
    // trigger()用于在值变化时设置新值(更新通知)
    trigger(target, key)
    return Reflect.set(...arguments)
  }
}

const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)

这样修改有以下原因 2. Object.defineProperty 只能劫持对象的属性,而 Proxy 是直接代理对象:由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性。而 Proxy 可以直接代理对象;proxy 不需要关心具体的 key,它去拦截的是 「修改 data 上的任意 key」 和 「读取 data 上的任意 key」。所以,不管是已有的 key  还是新增的 key,它都能提供拦截

new Proxy(data, {
 get(key) { },
 set(key, value) { },
})
  1. Object.defineProperty 对新增属性需要手动进行 Observe:在 2.x 版本中,给 data 中的数组或对象新增属性时,需要使用 vm.$set 才能保证新增的属性也是响应式的。这是由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持
  2. Object.defineProperty 无法监听数组变化
  3. 新标准:Proxy 作为 ES6 提供的新内置类,原生支持13种拦截操作。作为新标准,JS引擎会继续优化 Proxy ,但 getter 和 setter 基本不会再有针对性优化

vdom 重构

2.0 Vnode 性能瓶颈

  • 虽然 vue 能够保证触发更新的组件最小化,但单个组件部分变化需要遍历该组件的整个 vdom 树,静态结点只是可以跳过 patch 过程
  • 由于上条,传统 vdom 性能跟组件模版大小正相关,跟动态节点的数量无关。在一些组件整个模版内只有少量动态节点的情况下,这些遍历都是性能的浪费。

3.0 优化工作

vue3.0 做出优化,将 vdom 的操作颗粒度变小,每次触发更新不再以组件为单位进行遍历

  • 将 AST 基于动态节点指令(if,for,slot)切割为嵌套的区块 Blocks。每个区块只需要以一个 dynamicChildren 追踪自身包含的动态节点:给动态节点一个追踪标记 patchFlag,在渲染更新时只对这些动态节点进行 patch 和 diff 操作
  • 主动检测模版中的静态节点,最大化利用模版静态信息
- PatchFlag

以下代码可在 Vue 3 Template Explorer 尝试

Vue3.0 新特性 配图

在之前的 vdom 中,如果 msg 值发生改变,整个模版中的所有元素都需要重新渲染。但在 Vue3.0 中,在这个模版编译时,编译器会在动态标签末尾加上 /* Text*/ PatchFlag。只有带 PatchFlag 的 Node 才被认为是动态的元素,会被追踪属性的修改。并且 PatchFlag 会标识动态的属性类型有哪些,比如这里的 TEXT 表示只有节点中的文字是动态的。每一个 Block 中的节点,就算很深,也是直接跟 Block 一层绑定的,可以直接跳转到动态节点而不需要逐个逐层遍历。

  • hoistStatic
Vue3.0 新特性 配图

当使用 hoistStatic 时,所有静态的节点都被提升到 render 方法之外。这意味着,他们只会在应用启动的时候被创建一次,而后随着每次的渲染被不停的复用


Tree-shaking

因为ES6模块是静态引用的,所以可以在编译时判断到底加载了哪些代码。对代码全局做一个分析,找到那些没用被用到的模块、函数、变量,并把这些去掉

在 Vue2.x 版本中,无论从 Vue core 中使用了什么特性,都会在最后的生产代码中出现,因为 Vue 实例是作为一个单一对象导出的,而 bundlers 无法检测到这个对象的哪些属性在代码中使用了

 // Vue 2.x - whole `Vue` object is bundled for production
import Vue from 'vue'

Vue.nextTick(() => {})
const obj =  Vue.observable({})

Vue3.0 的源码将是 tree-shakeable 的,如果不使用一些功能(如 <keep-alive> 组件或 v-show 指令),它们可以不被包含在生产包中

 // Vue 3.x - only imported properties are bundled
import { nextTick, observable } from 'vue'

nextTick(() => {})
const obj = observable({})

Composition API

参考链接:

Options API

vue2.x 中,所有的数据都在 data()

中定义返回,方法定义在 methods 下面,并通过 this 调用——为了向 Vue 组件添加逻辑,需要填充 options,例如 data、methods、computed、watch 等。这种模式下的最大缺点是,这些填充的 options 本身并不是有效的 JavaScript 代码,需要确切了解模板中可以访问哪些属性,以及此关键字的行为。在后台,Vue编译器需要将此属性转换为工作代码

Composition API

vue3.x 中,所有的代码逻辑将在 setup() 中实现,包括 data、watch、computed、methods、hooks,并且不再有 this;setup() 在组件生命周期内只执行一次,不会重复执行

下面为使用组合 API 的例子:

<template>
  <button @click="increment">
    Count is: {{ count }}, double is {{ double }}, click to increment.
  </button>
</template>

<script>
import { ref, computed, onMounted } from 'vue'
export default {
  setup() {
    const count = ref(0)
    const double = computed(() => count.value * 2)
    watch(count, (newValue, oldValue) => {
        console.log('The new count value is: ' + count.value)
        })
    function increment() {
      count.value++
    }
    onMounted(() => console.log('component mounted!'))

    return {
      count,
      double,
      increment
    }
  }
}
</script>
  • setup() 只是一个函数,向模板返回属性和函数。在其中声明所有的响应式属性、计算属性、watchers 和生命周期钩子,然后返回,这样就可以在模板中使用。 没有从 setup() 中返回的东西将不能在模板中使用
  • 用 ref 创建响应式引用,ref 接受参数并返回它包装在具有 value property 的对象中,然后可以使用该 property 访问或更改响应式变量的值
  • 用 computed 创建 computed 属性
  • 用 watch 侦听响应式更改
  • 用 onMounted 创建 mount 生命周期钩子

Composition API:关注点分离

用组件的选项 (data、computed、methods、watch) 组织逻辑在大多数情况下都有效。然而,当组件变得更大时,逻辑关注点的列表也会增长,这可能会导致组件难以阅读和理解。这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,必须不断地“跳转”相关代码的选项块。

以文档的 UserRepositories.vue 为例(见官方文档:组合式 API),其实现了 2. 从外部 API 获取该用户名的仓库,并在用户更改时刷新它 4. 使用 searchQuery 字符串搜索存储库 6. 使用 filters 对象筛选仓库

但在 Options API 下,选项虽然分离,但实际上是不同关注点的代码散落在同一组件的不同选项中;使用 Composition API 时:

  • 目标一独立为 useUserRepositories.js 模块
  • 目标二独立为 useRepositoryNameSearch.js 模块
  • 目标三独立为 useRepositoryFilters.js 模块

之后在 UserRepositories.vue 中导入

// src/components/UserRepositories.vue
import { toRefs } from 'vue'
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import useRepositoryFilters from '@/composables/useRepositoryFilters'

通过 Composition API 可以将不同逻辑的实现代码提取到一个独立的组合式函数