Vue3新特性总结

https://v3-migration.vuejs.org/zh/ 参考Vue3迁移指南。

Composition API

Composition API是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它是一个概括性的术语,涵盖了以下方面的 API:

  • 响应式 API:例如 ref()reactive(),使我们可以直接创建响应式状态、计算属性和侦听器。
  • 生命周期钩子:例如 onMounted()onUnmounted(),使我们可以在组件各个生命周期阶段添加逻辑。
  • 依赖注入:例如 provide()inject(),使我们可以在使用响应式 API 时,利用 Vue 的依赖注入系统。
  • 在 Vue 3 中,组合式 API 基本上都会配合 <a href="https://cn.vuejs.org/api/sfc-script-setup.html">&lt;script setup&gt;</a> 语法在单文件组件中使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import { ref, onMounted } from 'vue'

// 响应式状态
const count = ref(0)

// 更改状态、触发更新的函数
function increment() {
count.value++
}

// 生命周期钩子
onMounted(() => {
console.log(`计数器初始值为 ${count.value}。`)
})
</script>

<template>
<button @click="increment">点击了:{{ count }} 次</button>
</template>

更好的逻辑复用

不同于Vue2中通过Mixins进行逻辑复用,通过组合式API我们可以通过组合函数(可以称为Vue Hooks)的形式,它完美解决了Mixins存在的一些问题。

  1. 不清晰的数据来源:当使用了多个 mixin 时,实例上的数据属性来自哪个 mixin 变得不清晰,这使追溯实现和理解组件行为变得困难。这也是我们推荐在组合式函数中使用 ref + 解构模式的理由:让属性的来源在消费组件时一目了然。
  2. 命名空间冲突:多个来自不同作者的 mixin 可能会注册相同的属性名,造成命名冲突。若使用组合式函数,你可以通过在解构变量时对变量进行重命名来避免相同的键名。
  3. 隐式的跨 mixin 交流:多个 mixin 需要依赖共享的属性名来进行相互作用,这使得它们隐性地耦合在一起。而一个组合式函数的返回值可以作为另一个组合式函数的参数被传入,像普通函数那样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div v-if="error">failed to load</div>
<div v-else-if="loading">loading...</div>
<div v-else>hello {{fullName}}!</div>
</template>

<script>
import { createComponent, computed } from 'vue'
import useSWR from 'vue-swr'

export default createComponent({
setup() {
// useSWR帮你管理好了取数、缓存、甚至标签页聚焦重新请求、甚至Suspense...
const { data, loading, error } = useSWR('/api/user', fetcher)
// 轻松的定义计算属性
const fullName = computed(() => data.firstName + data.lastName)
return { data, fullName, loading, error }
}
})
</script>

更灵活的代码组织

当项目非常巨大之后,Options API可能会给我们带来一些困扰:新增一个功能可能需要在data,method,template,watch,computed几处增加代码。如官网的示例(相同的逻辑关注点标上同一种颜色):

  1. 处理相同逻辑关注点的代码被强制拆分在了不同的选项中,位于文件的不同部分。
  2. 在一个几百行的大组件中,要读懂代码中的一个逻辑关注点,需要在文件中反复上下滚动,这并不理想。
  3. 如果我们想要将一个逻辑关注点抽取重构到一个可复用的工具函数中,需要从文件的多个不同部分找到所需的正确片段。

更好的类型推导

Vue2中使用TS会采用vue-class-component的方式,但是这种方式比较依赖装饰器,装饰器语法目前在ECMA Script经过一次很大的改动才进行到stage3.而Vue3开发的时候提案的进度是stage2。因此在Vue3中推荐使用变量和函数的形式来书写TS代码,获得更好的类型推导。

更小的生产包体积

搭配 <script setup> 使用组合式 API 比等价情况下的选项式 API 更高效,对代码压缩也更友好。

这是由于 <script setup> 形式书写的组件模板被编译为了一个内联函数,和 <script setup> 中的代码位于同一作用域。

不像选项式 API 需要依赖 this 上下文对象访问属性,被编译的模板可以直接访问 <script setup> 中定义的变量,无需一个代码实例从中代理。这对代码压缩更友好,因为本地变量的名字可以被压缩,但对象的属性名则不能。如下(通过terser压缩 https://try.terser.org/):

依赖this

1
2
3
4
5
6
7
8
9
const obj = {
name: 1,
sayName() {
console.log(this.name);
},
};
obj.sayName()
// 压缩后
({name:1,sayName(){console.log(this.name)}}).sayName();

内联函数

1
2
3
4
5
6
7
const name = 1
function sayName() {
console.log(name);
}
sayName()
// 压缩后
console.log(1);

对比React Hooks

React Hooks 在组件每次更新时都会重新调用,这会带来了一些性能问题,并且相当影响开发体验。例如:

  • Hooks 有严格的调用顺序,并不可以写在循环,条件分支中。
  • React 组件中定义的变量会被一个钩子函数闭包捕获,若开发者传递了错误的依赖数组,它会变得“过期”。这导致了 React 开发者非常依赖 ESLint 规则以确保传递了正确的依赖,然而,这些规则往往不够智能,保持正确的代价过高,在一些边缘情况时会遇到令人头疼的、不必要的报错信息。
  • 昂贵的计算需要使用 useMemo,这也需要传入正确的依赖数组。
  • 在默认情况下,传递给子组件的事件处理函数会导致子组件进行不必要的更新。子组件默认更新,并需要显式的调用 useCallback 作优化。这个优化同样需要正确的依赖数组,并且几乎在任何时候都需要。忽视这一点会导致默认情况下对应用进行过度渲染(不正确的使用优化函数导致),并可能在不知不觉中导致性能问题。
  • 要解决变量闭包导致的问题,再结合并发功能,使得很难推理出一段钩子代码是什么时候运行的,并且很不好处理需要在多次渲染间保持引用 (通过 useRef) 的可变状态。

相比起来,Vue 的组合式 API:

  • 仅调用 setup()<script setup> 的代码一次。这使得代码更符合日常 JavaScript 的直觉,不需要担心闭包变量的问题。组合式 API 也并不限制调用顺序,还可以有条件地进行调用。
  • Vue 的响应性系统运行时会自动收集计算属性和侦听器的依赖,因此无需手动声明依赖。
  • 无需手动缓存回调函数来避免不必要的组件更新。Vue 细粒度的响应性系统能够确保在绝大部分情况下组件仅执行必要的更新。

Setup语法糖

里面的代码会被编译成组件 setup() 函数的内容,因此 <script setup> 中的代码会在每次组件实例被创建的时候执行。

1
2
3
<script setup>
console.log('hello script setup')
</script>

顶层的绑定会被暴露给模板

当使用 <script setup> 的时候,任何在 <script setup> 声明的顶层的绑定 (包括变量,函数声明,以及 import 导入的内容) 都能在模板中直接使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
// import
import { capitalize } from './helpers'
// 变量
const msg = 'Hello!'
// 函数
function log() {
console.log(msg)
}
</script>

<template>
<button @click="log">{{ msg }}</button>
<div>{{ capitalize('hello') }}</div>
</template>

defineProps() 和 defineEmits()

为了在声明 propsemits 选项时获得完整的类型推导支持,我们可以使用 definePropsdefineEmits API,它们将自动地在 <script setup> 中可用:

1
2
3
4
5
6
7
8
<script setup>
const props = defineProps({
foo: String
})

const emit = defineEmits(['change', 'delete'])
// setup 代码
</script>

defineExpose()

使用 <script setup> 的组件是默认关闭的——即通过模板引用或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。

可以通过 defineExpose 编译器宏来显式指定在 <script setup> 组件中要暴露出去的属性:

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { ref } from 'vue'

const a = 1
const b = ref(2)

defineExpose({
a,
b
})
</script>

与普通的 <script> 一起使用

里面的代码会被编译成组件 setup() 函数的内容,因此在某些场景并不适用Setup而需要使用普通的 <script>

  • 声明无法在 <script setup> 中声明的选项,例如 inheritAttrs 或插件的自定义选项。
  • 声明模块的具名导出 (named exports)。
  • 运行只需要在模块作用域执行一次的副作用,或是创建单例对象。

Teleport

<Teleport> 是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去,类似React的Teleport组件,除了 <Teleport>,还新增了 <``Fragments``><Suspense>

如果遇到这种场景:一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方,那么 <Teleport>是一个很好的解决方案。

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import Modal from './Modal.vue'
import { ref } from 'vue'

const showModal = ref(false)
</script>

<template>
<button id="show-modal" @click="showModal = true">Show Modal</button>

<Teleport to="body">
<!-- 使用这个 modal 组件,传入 prop -->
<Modal :show="showModal" @close="showModal = false">
<template #header>
<h3>custom header</h3>
</template>
</Modal >
</Teleport>

</template>
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<script setup>
const props = defineProps({
show: Boolean
})
</script>

<template>
<Transition name="modal">
<div v-if="show" class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
<slot name="header">default header</slot>
</div>

<div class="modal-body">
<slot name="body">default body</slot>
</div>

<div class="modal-footer">
<slot name="footer">
default footer
<button
class="modal-default-button"
@click="$emit('close')"
>OK</button>
</slot>
</div>
</div>
</div>
</div>
</Transition>
</template>

<style>
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: table;
transition: opacity 0.3s ease;
}

.modal-wrapper {
display: table-cell;
vertical-align: middle;
}

.modal-container {
width: 300px;
margin: 0px auto;
padding: 20px 30px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
}

.modal-header h3 {
margin-top: 0;
color: #42b983;
}

.modal-body {
margin: 20px 0;
}

.modal-default-button {
float: right;
}

.modal-enter-from {
opacity: 0;
}

.modal-leave-to {
opacity: 0;
}

.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>

其他

  • css中允许使用v-bind指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <script setup>
    const theme = {
    color: 'red'
    }
    </script>

    <template>
    <p>hello</p>
    </template>

    <style scoped>
    p {
    color: v-bind('theme.color');
    }
    </style>
  • CSS Modules

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <template>
    <p :class="$style.red">This should be red</p>
    </template>

    <style module>
    .red {
    color: red;
    }
    </style>
  • 生态的改变
    Vue 3 的支持库进行了重大更新。以下是新的默认建议的摘要:

    • 新版本的 Router, Devtools & test utils 来支持 Vue 3
    • 构建工具链: Vue CLI -> Vite
    • 状态管理: Vuex -> Pinia
    • IDE 支持: Vetur -> Volar
    • 新的 TypeScript 命令行工具: vue-tsc
    • 静态网站生成: VuePress -> VitePress
    • JSX: @vue/babel-preset-jsx -> <a href="https://github.com/vuejs/babel-plugin-jsx">@vue/babel-plugin-jsx</a>
  • 非兼容性改变


Vue3新特性总结
https://jing-jiu.github.io/jing-jiu/2023/01/11/Framework/Vue3/Vue3新特性/
作者
Jing-Jiu
发布于
2023年1月11日
许可协议