Vue3笔记总结 组合式API与新特性完整梳理
这篇笔记是 2020 年底 Vue 3 正式发布那阵子整理的。写 Vue 2 的时候有个很磨人的体验,一个功能稍微复杂点的组件,同一件事的代码会被 options 的结构切成好几块,状态放 data,函数放 methods,派生值放 computed,副作用放 watch,想看清一条完整逻辑得在文件里上下翻好几趟。Composition API 就是冲着这件事来的。
下面把当时学的东西按顺序记一遍,从为什么要有 Composition API,到 setup、ref、reactive、生命周期、watch,再到 Suspense 和 Teleport 这两个新组件。另外这几年 Vue 3 的 API 定稿了不少,最后我单独加了一节,说明哪些写法已经变了。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- Vue 2 的 Options API 到底卡在哪,Composition API 解决的是什么
- Mixin、Mixin Factory、作用域插槽、Composition API 四种复用方案的取舍
setup的执行时机、两个参数,以及ref的装箱与自动拆箱reactive和toRefs的配合方式,以及什么时候该用哪个- 生命周期钩子在 Vue 3 里的对应关系和新增的两个调试钩子
watch与watchEffect的五种常见写法- 用组合函数封装 Promise 状态,实现跨组件的状态共享
- Suspense 处理异步加载和骨架屏,Teleport 把 DOM 传送到任意位置
- 这份 2020 年的笔记里,哪些写法到现在已经变了
# 一、为什么要有 CompositionAPI
# Vue2 的局限性
先说结论,Options API 不是写不了复杂组件,是写完之后没法读。具体卡在三个地方:
- 组件逻辑膨胀导致的可读性变差
- 无法跨组件重用代码
- Vue 2 对 TS 的支持有限
在传统的 Options API 中,我们需要把逻辑分散到以下六个部分:
componentspropsdatacomputedmethodslifecycle methods
问题不在于这六个格子本身,而在于它是按「代码的种类」分的,不是按「功能」分的。一个搜索框的逻辑,输入值在 data,防抖函数在 methods,过滤结果在 computed,请求触发在 watch。功能只有一个,代码却散在四个地方。组件小的时候没感觉,一旦一个组件里塞了搜索、排序、分页、导出四件事,这四条逻辑就在文件里彼此交织。
# 如何使用CompositionAPI解决问题
思路很直接,把同一个功能的代码聚到一起,可读性自然就回来了。
Composition API 是一个完全可选的语法,和原来的 Options API 并没有冲突。它让我们把相同功能的代码组织在一起,而不需要散落到 Options API 的各个角落。排序的状态、排序的方法、排序的计算属性,全部写在相邻的十几行里,读代码的人一眼扫过去就知道这块在干什么。
# 代码重用方法PK
聚合只解决了「一个组件内部」的问题。跨组件复用是另一件事。Vue 2 里我们大概有四个选择。
第一个是 Mixin 混入。
代码混入其实就是设计模式里的混合模式,缺点也非常明显。可以理解成多重继承,一个人如何有两个父亲。
缺点有两个:
- 无法避免属性名冲突
- 继承关系不清晰
属性名冲突这条是真的会咬人。你在组件里定义了一个 loading,混入的 mixin 里也有一个 loading,Vue 会按规则合并,但合并的结果往往不是你想要的。更麻烦的是继承关系不清晰,一个组件混了三个 mixin,模板里看到一个 handleSubmit,你得挨个翻文件才知道它是谁提供的。
第二个是 Mixin Factory 混入工厂,返回一个混入对象。
优点是代码重用方便,继承关系也清晰了,因为你能在调用工厂函数时把命名传进去。代价是模板变复杂,写起来啰嗦。
第三个是 ScopeSlots 作用域插槽。这个方案的问题比较集中:
- 可读性不高
- 配置复杂,需要在模板中进行配置
- 性能低,每个插槽相当于一个实例
第四个才是 Composition API 复合 API。
它的优势是这样几条:代码量少;没有引入新的语法,只是单纯的函数;异常灵活;工具语法提示友好,因为是单纯函数,所以很容易实现语法提示和自动补全。
最后这条我觉得被低估了。Mixin 和作用域插槽之所以对 TypeScript 不友好,是因为它们的组合发生在 Vue 的运行时,类型系统看不见。而组合函数就是一个普通函数调用,返回值的类型编辑器直接就能推出来。
# 二、setup 与 ref
# 使用CompositionAPI的理由
用它主要图三件事:更好的 TypeScript 支持;在复杂功能组件中可以按特性组织代码,比如把排序和搜索逻辑各自内聚成一块;组件间的代码复用。
# setup是什么
setup 在以下这些之前执行:
- Components
- Props
- Data
- Methods
- Computed Properties
- Lifecycle methods
它有两个好处。一是可以不再使用难于理解的 this,二是它有两个可选参数。
第一个参数是 props,它是一个响应式对象,而且可以被监听。
import {watch} from "vue"
export default {
props: {
name: String
},
setup(props) {
watch(() => {
console.log(props.name)
})
}
}
这里插一句,上面这段是 2020 年那会儿的写法,watch 传单个函数在当时能跑通。现在这种「不指定数据源、自动收集依赖」的用法对应的是 watchEffect,具体请以官方文档为准,我在第十二节还会展开说。
第二个参数是 context 上下文对象,用来代替以前 this 上能访问到的一些东西:
setup (props, context) {
const {attrs, slots, parent, root, emit} = context
}
这段也要提醒一句。parent 和 root 是 Vue 3 早期版本上的字段,正式版的 setup context 已经收敛了,具体暴露哪几个属性请以官方文档为准。这一节先按原来的理解往下走,不影响你理解 setup 的定位。
# ref是什么
对基本数据类型的数据进行装箱操作,使它成为一个响应式对象,从而可以跟踪数据变化。
为什么非得装箱?因为 JavaScript 的原始值是按值传递的,你把一个数字赋给另一个变量,两者之间没有任何联系,Vue 没有办法在它变化时收到通知。装进一个对象里就不一样了,ref 返回的是一个带 value 属性的对象,读 .value 时收集依赖,写 .value 时触发更新。
ref 的存在不是为了好看,是为了给原始值找一个能被追踪的壳。
# 小结
可维护性的提升体现在两点:可以控制哪些变量暴露出去,以及可以追踪哪些属性被定义过,也就是属性继承与引用透明。Options API 里 data 返回的每个字段都会自动挂到实例上,模板能用,混入能改;setup 里你返回什么,外面才能拿到什么。
# 三、Methods 与自动拆装箱
# 基础用法
方法在 setup 里就是一个普通的函数声明,不需要挂到 methods 下面,也不需要 this。用到哪个响应式变量就直接闭包引用它,最后一起 return 出去。
# 自动拆装箱
规则就两条:
- JS 里需要通过
.value访问包装对象 - 模板里自动拆箱
这里有个坑要注意。模板的自动拆箱只对 setup 返回的顶层 ref 生效。如果你返回的是一个普通对象,对象里面套了一个 ref,模板里访问这个嵌套字段是不会自动拆的,还得写 .value。我一开始也是这么想的,以为拆箱是全自动的,结果模板里渲染出来一个 [object Object],排查了一会儿才反应过来是嵌套层级的问题。
# 四、Computed 计算属性
这个地方实在没什么好讲的,和 Vue 2 没变化,只是从 options 的一个字段变成了从 vue 里 import 进来的函数。
<template>
<div>
<div>Capacity: {{ capacity }}</div>
<p>Spaces Left: {{ spacesLeft }} out of {{ capacity }}</p>
<button @click="increaseCapacity()">Increase Capacity</button>
</div>
</template>
<script>
import { ref, computed } from "vue";
export default {
setup(props, context) {
const capacity = ref(3);
const attending = ref(["Tim", "Bob", "Joe"]);
function increaseCapacity() {
capacity.value++;
}
const spacesLeft = computed(() => {
return capacity.value - attending.value.length;
});
return { capacity, increaseCapacity, attending, spacesLeft };
},
};
</script>
注意 computed 的回调里访问 capacity 必须写 .value,因为那是 JS 环境。模板里的 spacesLeft 不用写,因为那是模板环境。同一个变量,两种写法,刚上手确实容易搞混。
# 五、Reactive 响应式语法
之前我们用 ref 去声明所有的响应式属性:
import { ref, computed } from 'vue'
export default {
setup(){
const capacity = ref(4);
const attending = ref(["Tim","Bob","Joe"]);
const spacesLeft = computed(()=>{
return capacity.value - attending.value.length
})
function increaseCapacity(){ capacity.value++; }
return { capacity, increaseCapacity, attending, spacesLeft }
}
}
但是有另一个等效的方法,可以用它代替一堆 ref:
import { reactive, computed } from 'vue'
export default {
setup(){
const event = reactive({
capacity: 4,
attending: ["Tim","Bob","Joe"],
spacesLeft: computed(()=>{
return event.capacity - event.attending.length;
})
})
return { event }
}
}
过去我们用 Vue 2 的 data 来声明响应式对象,现在在这里每一个属性都是响应式的,包括 computed 计算属性。这两种方式相比,第二种没有到处写 .value,读起来清爽一些。
接下来再声明 method,两种语法都可以,取决于你选哪一种: