复用与组合对工程化的大项目更有意义,因此我们提前介绍了如何创建 Vue 3 项目,简单创建并创建使用了一些 SFC,接下来就来介绍如何实现 SPC 的复用与组合。
一,组合式 API 简介
(一)组合式 API 需要解决什么问题
假设我们的应用能显示某个用户的仓库列表,其中还有搜索和筛选功能,代码如下:
<script>
export default {
name: "UserRepositories",
// 三个子组件
components: {
RepositoriesList, // 列表,1
RepositoriesSearchBy, // 搜索,2
RepositoriesFilters, // 过滤,3
},
props: {
user: {
type: String,
required: true
}
},
data() {
return {
repositories: [], // 1
searchQuery: '', // 2
filters: {}, // 3
}
},
computed: {
filteredRepositories() { // 3
},
repositoriesMatchingSearchQuery() { // 2
},
},
watch: {
user: 'getUserRepositories' // 1
},
methods: {
getUserRepositories() { // 1
// 使用 `this.user` 获取用户仓库
},
updateFilters() { // 1
// 更新 `this.filters`
},
},
mounted() {
// 在实例挂载完成后调用方法获取用户仓库
this.getUserRepositories() // 1
}
}
</script>
该组件有以下几个职责:
- 从假定的外部 API 获取该用户的仓库,并在用户有任何更改时进行刷新
- 使用 searchQuery 字符串搜索仓库
- 使用 filters 对象筛选仓库
尽管在组件中使用 (data
、computed
、methods
、watch
) 组件的选项式 API 来组织逻辑关注点(上面代码中包含数字的注释)通常都很有效,就像以前我们做的那样。
然而,当我们的组件开始变得更大时,就完全有理由相信逻辑关注点的列表也会增长,这会导致组件难以阅读和维护:
- 逻辑关注点按颜色进行分组。
选项式 API 的这种代码分离,让我们在处理单个逻辑关注点时,需要经常不断地在相关代码的选项块之间跳转,这严重地然乱了逻辑顺序,进而掩盖了潜在的逻辑问题。
如果能够将同一个逻辑关注点相关代码收集在一起就好了,而这正是组合式 API 所能做的。
(二)组合式 API 基础
为了开始使用组合式 API,首先需要一个可以实际使用它的地方。在 Vue 组件中,我们将此位置称为 setup。
1,setup
组件选项
和软件测试的单元测试中设置固件时使用的 setUp()
方法一样,setup
选项在组件创建之前执行,一旦 props
被解析,setup
选项就将作为组合式 API 的入口。
- 因为
setup
的调用发生在props
被解析之后、在data
、computed
或methods
被解析之前,所以在setup
中无法使用this
。
setup
选项是一个接收 props
和 context
的函数,并能够将它返回的所有内容都暴露给组件的其余部分 (data
、methods
、computed
、watch
、生命周期钩子等等) 以及组件的模板。
举个例子🌰:
<body>
<div class="app">
</div>
<script>
const Root = {
setup() {
return {
name: 'Root',
handleClick: () => {
alert('done!');
},
}
},
template: `
<button @click="handleClick">点我!</button>
`,
};
const app = Vue.createApp(Root);
const vm = app.mount('.app');
</script>
回到教程👨💻,这就来把 setup
添加到组件中:
// src/components/UserRepositories.vue
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup(props) {
console.log(props) // { user: '' }
return {} // 这里返回的任何内容都可以用于组件的其余部分
}
// 组件的“其余部分”
}
首先来提取第一个逻辑关注点:从假定的外部 API 获取该用户的仓库,并在用户有任何更改时进行刷新。包含三个步骤:
- 获取仓库列表
- 更新仓库列表的函数
- 返回列表和函数
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
...
// 在我们的组件内
setup (props) {
let repositories = []
const getUserRepositories = async () => {
repositories = await fetchUserRepositories(props.user)
}
return {
repositories,
getUserRepositories // 返回的函数与 methods 的行为相同
}
}
...
我们设计了一个异步函数,这个函数通过一个函数从一个 API 中获取仓库数据,并将它们更新到一个数组变量中。
但还有个问题,因为 repositories 变量是非响应式的,这意味着从用户的角度来看,仓库列表将始终为空。
举个例子🌰:说明非响应式变量:
<div id="app"></div>
<script>
const Root = {
setup() {
let name = 'root';
// 每过 2 秒钟,更新一下 name
setInterval(() => {
name = name === 'root' ? 'ROOT' : 'root';
}, 2000);
return {
name
}
},
template: `
<p>{{ name }}</p>
`,
};
const app = Vue.createApp(Root);
const vm = app.mount('#app');
</script>
会发向并没有像我们期望的那样:每过 2 秒钟,更新一下 name。就是因为 name 是非响应式的。
2,用 ref 或 reactive 创建响应式变量
在 Vue 3.0 中,可以通过一个 ref
函数或 reactive
函数创建响应式变量,从而使得这个变量能在任何地方响应式地起作用。
- 响应式原理,请参考Reactivity in Depth
let name = 'root';
let reactivityName = ref(name);
ref
为我们要使用的基础类型的数据(比如字符串、数字)创建了一个响应式引用。- 非基础类型的数据(数组、对象、Map 和 Set )可使用
reactive
来处理。
但比较特殊的是,变量在作为参数传入到 ref
函数后,会被包裹在一个带有 value
property 的对象(proxy({value:0})
)中返回。
将值封装在一个对象中,看似没有必要,但为了保持 JavaScript 中不同数据类型的行为统一,这是必须的。这是因为在 JavaScript 中,Number 或 String 等基本类型是通过值而非引用传递的:
将变量封装为引用对象后,就可以使用 value
property 访问或更改 ref
响应式变量的值:
<div class="app"></div>
<script>
const Root = {
setup() {
const {ref} = Vue; // 只在html中这么用,SFC 中提前用 import { ref } from 'vue' 导入,处理简单数据类型
const {reactive} = Vue; // 用 reactive 处理数组与对象
let name = 'root';
let nameObj = {
name: 'ROOT'
};
let reactivityName = ref(name); // 包装为响应式数据
let reactivityNameObj = reactive(nameObj); // 包装为响应式数据
// 每过 1 秒钟,更新一下 name
setInterval(() => {
reactivityName.value = reactivityName.value === 'root' ? 'ROOT' : 'root'; // 使用 .value 获取或变更 ref 响应式数据的在值
reactivityNameObj.name = reactivityNameObj.name === 'root' ? 'ROOT' : 'root';
}, 1000);
return { // 返回的数据会被自动挂载到组件实例上
reactivityName,
reactivityNameObj
}
},
// 在 DOM 中直接<p>{{ reactivityName.value }}</p>不会显示变化,因为 vue 会自动浅层次解包内部值,
// 只有访问嵌套的 ref 时需要在模板中添加 .value,具体详见 Vue 官网文档:响应式状态解构
// 在模板中可直接像下面这样用:
template: `
<p>{{ reactivityName }}</p>
<p>{{ reactivityNameObj.name }}</p>
`,
};
const app = Vue.createApp(Root);
const vm = app.mount('.app');
</script>
用 ref
或 reactive
创建响应式变量可一定程度上代替 data
property。
回到教程👨💻,这就来将 repositories 变量创建为响应式变量:
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'
...
// 在我们的组件中
setup (props) {
const repositories = reactive([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
return {
repositories,
getUserRepositories
}
}
...
从此开始,每当我们调用 getUserRepositories 时,如果结果有变化,则 repositories 将发生同样的变化。
- 如果需要,完全可以使用
readonly
函数将变量变为只读变量。
3,在 setup 内使用生命周期钩子
接口准备好了,调用接口的函数准备好了,存储接口数据的响应式变量也准备好了,现在就来执行仓库获取逻辑吧。
那么,在哪里调用查询函数 getUserRepositories 呢?
在这个系列的最开始,我们说过,每个组件在被创建时都要经过一系列的初始化过程,在初始化过程中就会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会:
从这张生命周期图中我们能看到正在使用的 setup
函数。
为了使组合式 API 的功能和选项式 API 一样完整,我们还需要一种在 setup
中使用生命周期钩子的方法。
恰恰 Vue 也提供这些方法。这些函数接受一个回调,当钩子被组件调用时,该回调将被执行。组合式 API 上的生命周期钩子与选项式 API 的名称相同,但需要加个前缀 on
,即 mounted
看起来会像 onMounted
。
回到教程👨💻,这里就在 setup
中使用 onMounted
来回调查询函数 getUserRepositories:
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted } from 'vue'
// 在我们的组件中
setup (props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
onMounted(getUserRepositories) // 在 `mounted` 时调用 `getUserRepositories`
return {
repositories,
getUserRepositories
}
}
目前,我们已经将第一个逻辑关注点中的几个部分移到了 setup
方法中,在代码结构中它们彼此非常接近。
4,watch 响应式更改
因为查询函数使用父组件传入的 user 作为参数,显然我们需要为不同的 user 进行查询操作,所以就需要随时监听 user 的变化。
就像我们在组件中使用 watch
选项监听 data
中的 user 一样,我们也可以使用从 Vue 导入的 watch
函数执行相同的操作。它接受 3 个参数:
- 一个想要侦听的响应式引用或 getter 函数
- 一个变化时的回调
- 其他可选的配置选项
举个例子🌰:
import { ref, watch } from 'vue'
// 在我们的组件中
...
const counter = ref(0)
watch(counter, (newValue, oldValue) => {
console.log('The new counter value is: ' + counter.value)
})
每当 counter 被修改时,例如 counter.value=5,watch
将触发并执行回调 (第二个参数),在本例中,它将把 ‘The new counter value is:5’ 记录到控制台中。
等效的选项式 API:
data() {
return {
counter: 0
}
},
watch: {
counter(newValue, oldValue) {
console.log('The new counter value is: ' + this.counter)
}
}
回到教程👨💻,
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs } from 'vue'
// 在我们组件中
setup (props) {
const { user } = toRefs(props) // 从父组件传入的用户信息
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
watch(user, getUserRepositories)
return {
repositories,
getUserRepositories
}
}
至此,我们已经把第一个逻辑关注点移到了同一个地方,看一下整体代码:
<template>
</template>
<script>
import {fetchUserRepositories} from '@/api/repositories'
import {ref, onMounted, watch, toRefs} from 'vue'
export default {
name: "UserRepositories",
components: {RepositoriesFilters, RepositoriesSortBy, RepositoriesList},
props: {
user: {
type: String,
required: true
}
},
// 在我们的组件内
setup(props) {
// 使用 `toRefs` 解包的方法创建对 `props` 中的 `user` property 的响应式引用
const {user} = toRefs(props)
// 创建一个响应式的 `repositories` 数组变量,准备存储从外部 API 获取到的仓库信息列表
const repositories = ref([])
// 初始化本地仓库列表数据
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(user.value)
}
// 在组件挂载时获取数据
onMounted(getUserRepositories);
// 在 user prop 的响应式引用上设置一个侦听器,用于根据用户信息获取对应的仓库列表信息
watch(user, getUserRepositories)
return {
repositories, // 返回一个响应式变量,
getUserRepositories // 返回的函数与方法的行为相同
}
}
}
</script>
<style scoped>
</style>
5,独立的 computed 属性
我们现在可以对第二个逻辑关注点执行相同的操作——基于 searchQuery 进行过滤。
在选项式 API 中,我们介绍过 computed
property,我们可用它来简化模板中的计算运算。而且使用 computed
property 的一个好处就是,它将基于响应依赖关系进行计算缓存,只会在相关响应式依赖发生改变时才重新求值。
而组合式 API 也提供了 computed
函数来在 Vue 组件外部创建计算属性。
举个例子🌰:
<div class="app"></div>
<script>
const Root = {
setup() {
const {ref, computed} = Vue;
const counter = ref(0);
const handClick = () => {
counter.value += 1;
};
const counterAddFive = computed(() => {
return counter.value * 2;
});
return {
counter, // 曝露出去的状态数据
counterAddFive, // 曝露出去的计算属性
handClick // 曝露出去的方法
}
},
template: `
<button @click="handClick">{{ counter }}</button>——<span> {{ counterAddFive }} </span>
`,
};
const app = Vue.createApp(Root);
const vm = app.mount('.app');
</script>
- 这里我们给
computed
函数传递了一个箭头函数作为参数,它是一个类似 getter 的回调函数,输出的是一个只读的响应式引用。为了访问新创建的计算变量的 value,我们需要像ref
一样使用.value
property。
回到教程👨💻,将搜索功能移到 setup
中:
// src/components/UserRepositories.vue
<template>
</template>
<script>
import {fetchUserRepositories} from '@/api/repositories'
import {ref, onMounted, watch, toRefs, computed} from 'vue'
export default {
name: "UserRepositories",
components: {RepositoriesFilters, RepositoriesSortBy, RepositoriesList},
props: {
user: {
type: String,
required: true
}
},
// 在我们的组件内
setup(props) {
/*关注点一*/
// 使用 `toRefs` 解包的方法创建对 `props` 中的 `user` property 的响应式引用
const {user} = toRefs(props)
// 创建一个响应式的 `repositories` 数组变量,准备存储从外部 API 获取到的仓库信息列表
const repositories = ref([])
// 初始化本地仓库列表数据
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(user.value)
}
// 在组件挂载时获取数据
onMounted(getUserRepositories);
/*关注点二*/
// 在 user prop 的响应式引用上设置一个侦听器,用于根据用户信息获取对应的仓库列表信息
watch(user, getUserRepositories)
/*关注点三*/
// 创建一个响应式的字符串,用作过滤条件
const searchQuery = ref('')
// 通过 `computed` 创建一个响应式的函数,用于过滤数据
const repositoriesMatchingSearchQuery = computed(() => {
return repositories.value.filter(
repository => repository.name.includes(searchQuery.value) // 根据 searchQuery 进行仓库过滤
)})
return {
repositories, // 曝露出去的状态数据,仓库列表
getUserRepositories, // 曝露出去的方法,用于初始化仓库列表
searchQuery, // 曝露出去的状态数据,查询条件
repositoriesMatchingSearchQuery // 曝露出去的方法,用于查询仓库
}
}
}
</script>
<style scoped>
</style>
这样一来,我们已经将所有逻辑关注点都放到 setup
中了,但这又让 setup
变得比较臃肿,
因此,我们进一步模块化代码,将各个关注点提取到一个个独立的组合式函数中,作具体的功能进行分组。
让我们从创建 useUserRepositories 函数开始:
// src/utils/useUserRepositories.js
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch } from 'vue'
export default function useUserRepositories(user) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
watch(user, getUserRepositories)
return {
repositories,
getUserRepositories
}
}
然后是搜索功能:
// src/utils/useRepositoryNameSearch.js
import { ref, computed } from 'vue'
export default function useRepositoryNameSearch(repositories) {
const searchQuery = ref('')
const repositoriesMatchingSearchQuery = computed(() => {
return repositories.value.filter(repository => {
return repository.name.includes(searchQuery.value)
})
})
return {
searchQuery,
repositoriesMatchingSearchQuery
}
}
现在我们有了两个单独的功能模块,接下来就可以开始在组件中使用它们了:
// src/utils/UserRepositories.vue
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import { toRefs } from 'vue'
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup (props) {
// 创建一个响应式对象,并解包
const { user } = toRefs(props)
// 获取用户仓库列表
const { repositories, getUserRepositories } = useUserRepositories(user)
// 获取仓库名称搜索
const {
searchQuery,
repositoriesMatchingSearchQuery
} = useRepositoryNameSearch(repositories)
return {
// 因为我们并不关心未经过滤的仓库
// 所以我们在 `repositories` 名称下只暴露过滤后的结果
repositories: repositoriesMatchingSearchQuery,
getUserRepositories,
searchQuery,
}
}
}
最后,迁移剩余的过滤功能(这里就不关心怎么实现的):
// src/utils/UserRepositories.vue
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import useRepositoryFilters from '@/composables/useRepositoryFilters'
import {toRefs} from 'vue'
export default {
components: {RepositoriesFilters, RepositoriesSortBy, RepositoriesList},
props: {
user: {
type: String,
required: true
}
},
setup(props) {
// 创建一个响应式对象,并解包
const {user} = toRefs(props)
// 获取用户仓库列表
const {repositories, getUserRepositories} = useUserRepositories(user)
// 获取仓库名称搜索
const {
searchQuery,
repositoriesMatchingSearchQuery
} = useRepositoryNameSearch(repositories)
// 获取仓库过滤器
const {
filters,
updateFilters,
filteredRepositories
} = useRepositoryFilters(repositoriesMatchingSearchQuery)
return {
// 因为我们并不关心未经过滤的仓库
// 所以我们在 `repositories` 名称下只暴露过滤后的结果
repositories: filteredRepositories,
getUserRepositories,
searchQuery,
filters,
updateFilters
}
}
}
Done!至此,我们:
- 将选项式 API 重构为组合式 API,实现逻辑关注点分离。
- 对组合式 setup() 做功能拆分,进一步简化代码复杂度,同时有利于对拆分出的内容进行进一步的复用。
总之,组合式 API 的核心诉求就是提高组件、逻辑的复用性。
接下来就把用到的一些重要概念和细节拆开来看看。
二,Setup
(一)参数说明
1,props
先回忆一下,组件中的 props
property 接收来自父组件的数据,然后整个组件中都可使用。
这里之所以将响应式的 props
作为第一个参数传入 setup
函数,是为了方便在 setup
函数中使用其中的一些数据来完成某些初始化工作(数据状态的初始化以及方法的准备)。
src/App.vue:
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld name="root" msg="welcome to your Vue.js App"/>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
需要注意的是,在 setup
函数中不能使用 ES6 语法解包这个参数,它会消除 prop 的响应性,而应该使用 toRefs
函数来完成此操作——为响应式的对象提供解包/展开能力:
// src/components/HelloWorld.vue
<template>
<div class="hello">
<h1>{{name}}, {{ msg }}</h1>
</div>
</template>
<script>
import {toRef, toRefs} from "vue";
export default {
name: 'HelloWorld',
props: {
name:String,
msg: String
},
setup(props) {
// 输出 props 的 prop 值
console.log(props.name, props.msg)
// 使用 toRefs() 将props转换为可解包的对象
const {name, msg} = toRefs(props)
console.log(name.value, msg.value)
}
}
</script>
还可以使用 toRef()
函数创建本处需要但可能未传入的 prop —— 为对象的某个属性提供响应式能力。
// src/components/HelloWorld.vue
<template>
<div class="hello">
<h1>{{name}}, {{ msg }}</h1>
</div>
</template>
<script>
import {toRef, toRefs} from "vue";
export default {
name: 'HelloWorld',
props: {
name:String,
msg: String
},
setup(props) {
// 使用 toRef() 创建对象的某属性的响应式形式
const title = toRef(props, 'title', '<TITLE>')
console.log(title.value)
}
}
</script>
2,Context
传递给 setup
函数的第二个参数是 context
。context
是一个普通 JavaScript 对象,是非响应式的,可以是 attrs, slots, emit, expose:
<script>
export default {
name: 'HelloWorld',
props: {
name: String,
msg: String
},
setup(props, context) {
// Attribute (非响应式对象,等同于 $attrs)
console.log(context.attrs)
// 插槽 (非响应式对象,等同于 $slots)
console.log(context.slots)
// 触发事件 (方法,等同于 $emit)
console.log(context.emit)
// 暴露公共 property (函数)
console.log(context.expose)
}
}
</script>
因为 attrs
property 和 slots
property 是非响应式的。如果你打算根据 attrs
或 slots
的变更应用副作用,那么应该在 onBeforeUpdate
生命周期钩子中执行此操作。
总之,执行 setup
时,你只能访问props
、attrs
、slots
、emit
(二)在模板使用暴露出来的内容
如果 setup
返回一个对象,那么该对象的 property 以及传递给 setup
的 props
参数中的 property 就都可以在模板中访问到:
src/App.vue
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld collectionName="Vue 3 Guide"></HelloWorld>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
src/components/HelloWorld.vue
<template>
<p> {{ collectionName }} ({{ version }}):</p>
<div v-for="(blogInfo,key) in blogsInfo" :key="key">
{{ blogInfo.title }}
<span>{{ blogInfo.likes }}</span>
</div>
</template>
<script>
import {ref, reactive} from 'vue'
export default {
props: {
collectionName: String
},
setup(props) {
console.log(props)
const version = ref("v1.2.3")
const blogs = [
{id: 1, title: 'My journey with Vue😘', likes: 23},
{id: 2, title: 'Blogging with Vue🚄', likes: 14},
{id: 3, title: 'Why Vue is so interesting🥰', likes: 100}
]
const blogsInfo = reactive(blogs)
// 暴露给 template
return {
version,
blogsInfo
}
}
}
</script>
之前也说过,从 setup
返回的 refs
在模板中访问时是被自动浅解包的,因此不应在模板中使用 .value
而应该直接使用变量。
(三)使用 <script setup>
简化 setup()
三,生命周期钩子
四,Provide / Inject
Provide / Inject
的存在简化了嵌套组件之间的自顶向下的数据传递过程。
(一)在 setup 内部使用 Provide / Inject
通过一个例子说明: 这里有一个 MyMap 组件,该组件使用组合式 API 为 MyMarker 子组件提供用户的位置。
在 setup
中使用 provide
时,首先从 vue 显式导入 provide
方法:
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup() {
provide('location', 'North Pole')
provide('geolocation', {
longitude: 90,
latitude: 135
})
}
}
</script>
在 setup
中使用 inject
时,也需要显式导入 inject
方法::
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'
export default {
setup() {
const userLocation = inject('location', 'The Universe')
const userGeolocation = inject('geolocation')
return {
userLocation,
userGeolocation
}
}
}
</script>
(二)响应式 provide / inject
为了增加 provide
值和 inject
值之间的响应性,我们可以在 provide 值时使用 ref
或 reactive
(使用前者还是后者,在最上面讲过):
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
provide('location', location)
provide('geolocation', geolocation)
}
}
</script>
这样一来,当父组件 MyMap 提供的位置信息变化时,子组件 MyMarker 也将自动更新!
当使用响应式 provide / inject
值时,官方建议尽可能将对响应式 property 的所有修改限制在定义 provide
所在的组件的内部。
例如在需要更改用户位置的情况下,我们最好在 MyMap 组件中执行此操作:
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
provide('location', location)
provide('geolocation', geolocation)
return {
location
}
},
// 修改位置
methods: {
updateLocation() {
this.location = 'South Pole'
}
}
}
</script>
如果需要在注入数据的组件内部更新 inject
的数据,官方建议 provide 一个方法来负责响应式变更:
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
// 变更方法
const updateLocation = () => {
location.value = 'South Pole'
}
provide('location', location)
provide('geolocation', geolocation)
// provide 变更方法
provide('updateLocation', updateLocation)
}
}
</script>
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'
export default {
setup() {
const userLocation = inject('location', 'The Universe')
const userGeolocation = inject('geolocation')
const updateUserLocation = inject('updateLocation')
return {
userLocation,
userGeolocation,
// 暴露出变更方法
updateUserLocation
}
}
}
</script>
如果要确保通过 provide
传递的数据不会被 inject
的组件更改,官方建议对 provideer 的 property 使用 readonly
函数:
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, readonly, ref } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
const updateLocation = () => {
location.value = 'South Pole'
}
// 只读
provide('location', readonly(location))
provide('geolocation', readonly(geolocation))
provide('updateLocation', updateLocation)
}
}
</script>
总之,组合式 API 允许我们分离关注点,通过对大型的 SFC内部的逻辑进行切割与整合,有利于后期的代码维护。
五,组合式 API 的响应式
(一)响应式对象的创建与解包
上面将国,reactive()
会创建一个响应式的对象、数组 Map 和 Set 这样的集合类型,但实际上它就是返回一个原始对象的响应式代理对象,它和原始对象是不相等的:
const raw = {}
const proxy = reactive(raw)
// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true
也就是说,只有代理对象是响应式的,更改原始对象不会触发更新。因此,为获得响应式能力,我们应该使用代理对象而不是原始对象。
使用组合式 API 的问题就是 ref 和响应式对象到底用哪个:
- 原始响应式对象存在解构丢失响应性的问题,而代理对象需要到处使用
.value
则感觉很繁琐,并且在没有类型系统的帮助时很容易漏掉.value
。
更多组合式 API 中的响应式内容请参考 响应式基础。
(二)使用 <script setup>
在 setup()
函数中手动返回老暴露大量的状态和方法可能会非常繁琐,可以使用 <script setup>
来大幅度地简化代码:
<script setup>
中的顶层的导入、函数和变量声明等可在同一组件的模板中直接使用。可以理解为模板中的表达式和<script setup>
中的代码处在同一个作用域中——<script setup>
里面的代码会被编译成组件setup()
函数的内容。
<template>
<MyComponent />
<div>{{ capitalize('hello') }}</div>
<button @click="increment">
{{ state.count }}
</button>
</template>
<script setup>
import {reactive} from 'vue'
import { capitalize } from './helpers'
import MyComponent from './MyComponent.vue'
const state = reactive({count: 0}) // reactive() is a function that returns a reactive object
// a function that increments the count property of the reactive state object.
// this type of function is always called as a event handler.
function increment() {
state.count++
}
</script>
使用 <script setup>
时没有明显的 props
参数,所以需要 defineProps()
函数和 defineEmits()
函数来进行显式的声明:
<script setup>
const props = defineProps({
foo: String
})
const emit = defineEmits(['change', 'delete'])
</script>
尽管在 <script setup>
使用 slots
和 attrs
的情况应该是相对来说较为罕见的——因为可以在模板中直接通过 $slots
和 $attrs
来访问。但确实要使用的话,可以通过 useSlots()
函数和 useAttrs()
函数来获取:
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>
官方推荐在组合式 API 中使用 SFC + <script setup>
的语法。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/98047.html