Vue3之Composition API详解 从setup到组合式函数
一个业务组件写到八百行,data 里堆了二十个字段,methods 里躺着三十个函数,你想改「购物车优惠券」这一块的逻辑,得在文件里上下横跳七八次,因为它的状态在第 30 行,计算在第 210 行,方法在第 480 行,监听在第 700 行。这不是代码写得烂,是选项式 API 按「选项类型」而不是按「业务关注点」组织代码的必然结果。组合式 API 就是冲着这件事来的。这篇把 setup、ref、reactive、toRefs、computed、watch、watchEffect、生命周期钩子和 provide / inject 逐个拆开讲,重点不在 API 签名,而在什么时候用哪个、边界在哪。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
setup的执行时机、两个参数,以及它为什么没有thisref和reactive到底该用哪个,reactive的三个坑toRefs怎么让解构不丢响应性,这是组合式函数的关键一环computed和readonly的典型用法watchEffect自动收集依赖,watch手动指定源,两者的四点区别- 组合式 API 的生命周期钩子改名规律和注册时机
provide/inject跨层级传值,以及怎么保住响应性- 把逻辑抽成组合式函数,这才是组合式 API 存在的理由
- 关于
script setup,2021 年它还是实验特性,现在已经是推荐写法
Composition API 也叫组合式 API,是 Vue 3.x 的新特性。
官方文档里的说法是这样的:通过创建 Vue 组件,我们可以将接口的可重复部分及其功能提取到可重用的代码段中,仅此一项就能让应用在可维护性和灵活性上走得更远。但经验证明光靠这一点可能不够,尤其是当你的应用变得非常大的时候,比如上百个组件的规模。在处理这么大的应用时,共享和重用代码变得尤为重要。
通俗的讲:没有 Composition API 之前,Vue 相关业务的代码需要配置到 option 的特定区域,中小型项目没有问题,但在大型项目中会导致后期维护复杂,同时代码可复用性不高。Vue 3.x 中的 composition-api 就是为了解决这个问题而生的。
compositon api 提供了以下几个函数:
setuprefreactivewatchEffectwatchcomputedtoRefs- 生命周期的
hooks
# 一、setup组件选项
新的 setup 组件选项在创建组件之前执行,一旦 props 被解析,它就充当组合式 API 的入口点。
这里有个提示要记住:由于在执行 setup 时尚未创建组件实例,因此在 setup 选项中没有 this。所以除了 props 之外,你无法访问组件中声明的任何属性,本地状态、计算属性、方法一个都拿不到。
使用 setup 函数时,它会接受两个参数:
propscontext
下面分别看这两个参数怎么用。
# 1. Props
setup 函数中的第一个参数是 props。正如在一个标准组件中所期望的那样,setup 函数中的 props 是响应式的,当传入新的 prop 时,它会被更新。
// MyBook.vue
export default {
props: {
title: String
},
setup(props) {
console.log(props.title)
}
}
注意:因为 props 是响应式的,你不能使用 ES6 解构,解构会消除 prop 的响应性。
这个坑很多人踩过。const { title } = props 之后,title 就是一次性取出来的普通字符串,父组件再怎么改,你手里这个变量都不会动。如果需要解构 prop,可以通过使用 setup 函数中的 toRefs 来安全地完成此操作:
// MyBook.vue
import { toRefs } from 'vue'
setup(props) {
const { title } = toRefs(props)
console.log(title.value)
}
这里有个额外的坑:如果某个 prop 是可选的、父组件没传,toRefs 在那个 key 上拿不到东西,这种情况用 toRef(props, 'title') 更稳,它对不存在的 key 也能建立引用。
顺便说个时效性的事。新版本 Vue 对 defineProps 的解构做了编译期处理,让解构后的变量也能保持响应性,不再需要处处包 toRefs。具体从哪个版本开始默认开启、有哪些限制,以官方文档为准。
# 2. 上下文
传递给 setup 函数的第二个参数是 context。context 是一个普通的 JavaScript 对象,它暴露三个组件的 property:
// MyBook.vue
export default {
setup(props, context) {
// Attribute (非响应式对象)
console.log(context.attrs)
// 插槽 (非响应式对象)
console.log(context.slots)
// 触发事件 (方法)
console.log(context.emit)
}
}
context 是一个普通的 JavaScript 对象,它不是响应式的,所以你可以安全地对 context 使用 ES6 解构:
// MyBook.vue
export default {
setup(props, { attrs, slots, emit }) {
...
}
}
不过 attrs 和 slots 是有状态的对象,它们总是会随组件本身的更新而更新。所以你应该避免对它们再往下解构,始终以 attrs.x 或 slots.x 的方式引用 property。请注意,与 props 不同,attrs 和 slots 是非响应式的。如果你打算根据 attrs 或 slots 做副作用,那应该在 onUpdated 生命周期钩子中执行。
这三个之外,context 上还有一个 expose,用来显式声明这个组件对外暴露哪些属性和方法,父组件通过模板 ref 拿到的就是你 expose 出去的那些。
# 3. setup 组件的 property
执行 setup 时,组件实例尚未被创建。因此,你只能访问以下 property:
propsattrsslotsemit
也就是说,你将无法访问以下组件选项:
datacomputedmethods
这个限制不是故意为难人,它是执行顺序决定的。setup 跑在 beforeCreate 和 created 之间,那时候 data() 还没被调用,computed 还没被求值,自然什么都没有。
# 4. ref reactive 以及 setup 结合模板使用
在看 setup 结合模板使用之前,我们首先得知道 ref 和 reactive 方法。
如果 setup 返回一个对象,则可以在模板中绑定对象中的属性和方法。但要定义响应式数据的时候,需要用 ref 或 reactive 方法。
# 错误写法
下面这段是最典型的入门错误,msg 是个普通字符串,点按钮之后 alert 会弹,值也确实改了,但页面纹丝不动:
<template>
{{msg}}
<br>
<button @click="updateMsg">改变setup中的msg</button>
<br>
</template>
<script>
export default {
data() {
return {
}
},
setup() {
let msg = "这是setup中的msg";
let updateMsg = () => {
alert("触发方法")
msg = "改变后的值"
}
return {
msg,
updateMsg
}
},
}
</script>
为什么改了值页面不更新呢?因为 return 出去的那一刻,msg 的值就被拷贝进返回对象了,之后你改的是 setup 作用域里那个局部变量,和模板拿到的那份已经没有关系。Vue 也没法在这中间插进去,一个普通字符串上没有任何可以被劫持的入口。这块的底层机制我在 Vue响应式原理模拟 里从 Object.defineProperty 和 Proxy 讲起手写了一遍。
# 正确写法一 ref
ref 用来定义响应式的字符串、数值、数组、Bool 类型:
import {
ref
} from 'vue'
模板部分照常写,注意模板里用 msg 不用带 .value:
<template>
{{msg}}
<br>
<br>
<button @click="updateMsg">改变setup中的msg</button>
<br>
<br>
<ul>
<li v-for="(item,index) in list" :key="index">
{{item}}
</li>
</ul>
<br>
</template>
脚本里定义和修改都要走 .value:
<script>
import {
ref
} from 'vue'
export default {
data() {
return {
}
},
setup() {
let msg = ref("这是setup中的msg");
let list = ref(["马总", "李总", "刘总"])
let updateMsg = () => {
alert("触发方法");
msg.value = "改变后的值"
}
return {
msg,
list,
updateMsg
}
},
}
</script>
ref 的原理就是包一层。它返回一个带 value 属性的对象,读写都过这个属性的 getter/setter,这样 Vue 就有地方做依赖收集和触发更新了。模板里之所以不用写 .value,是因为渲染的时候 Vue 会对顶层的 ref 自动解包。这个自动解包只在模板的顶层属性上生效,把 ref 塞进数组或者 Map 里再取出来,还是得自己写 .value。
# 正确写法二 reactive
reactive 用来定义响应式的对象:
import {
reactive
} from 'vue'
模板里 reactive 定义的对象直接按属性路径访问:
<template>
{{msg}}
<br>
<br>
<button @click="updateMsg">改变setup中的msg</button>
<br>
<br>
<ul>
<li v-for="(item,index) in list" :key="index">
{{item}}
</li>
</ul>
<br>
{{setupData.title}}
<br>
<button @click="updateTitle">更新setup中的title</button>
<br>
<br>
</template>
脚本里 reactive 返回的是一个 Proxy 代理对象,改属性不需要 .value: