本节是Vue3相对于Vue2在使用上的最大变化。
但必须强调,组合式API不是强制使用的,你完全可以不用。
通过创建 Vue 组件,我们可以将页面中重复的部分连同其功能一起提取并封装为可重用的代码段。
这样不但提高了开发效率,而且在可维护性和灵活性方面相当有帮助。
想想拥有几百个组件的大型应用,共享和重用代码变得尤为重要。
然而,工作实践告诉我们,光靠封装组件可能并不够。
看下面这个例子,假设有一个显示某个用户的仓库列表的页面,并且它具有搜索和筛选功能。
实现此页面的组件代码可能如下所示(这里有一些单文件组件的语法,可以先不关心):
// src/components/UserRepositories.vue export default { components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList }, props: { user: { type: String, required: true } }, data () { return { repositories: [], // 1 b filters: { ... }, // 3 searchQuery: '' // 2 } }, computed: { filteredRepositories () { ... }, // 3 repositoriesMatchingSearchQuery () { ... }, // 2 }, watch: { user: 'getUserRepositories' // 1 }, methods: { getUserRepositories () { // 使用 `this.user` 获取用户仓库 }, // 1 updateFilters () { ... }, // 3 }, mounted () { this.getUserRepositories() // 1 } }
该组件有以下几个功能:
searchQuery
字符串搜索仓库filters
对象筛选仓库使用 (data
、computed
、methods
、watch
) 组件选项的方式来组织代码逻辑通常都很有效(这也是Vue2一直使用的模式)。
然而,当我们的组件开始变得更大时,逻辑关注点的列表也会急剧增长。对于那些一开始没有参与编写这些组件的人来说,这些组件变得难以阅读和理解,如下图所示:
上面是一个大型组件的示例,其中逻辑关注点按颜色进行分组。
这种代码组织方式,使得理解和维护,复杂的组件变得困难。不同选项的分离掩盖了潜在的逻辑问题。
此外,在处理单个逻辑关注点时,我们还必须不断地“跳转”相关代码的选项块。
如果能够将同一个逻辑关注点相关代码收集在一起会更好,而这正是组合式 API 存在的核心目的。
通俗地解释一下上面的内容:有些程序员觉得把代码分别写在data、computed、methods等选项卡中不利于维护和理解,希望可以按照功能组合代码。组合式API不是为了提高性能,也不是带来新特性,而是方便程序员看代码维护代码。
为了使用组合式 API,我们首先需要一个可以实际使用它的地方(也就是代码写在哪里)。
在 Vue 组件中,我们将此位置称为 setup
。
setup
组件选项你没看错,为了解决选项的问题,我们又创建了一个新的选项。
之所以叫做setup,有启动、一开始、预先配置的意思。
因为
setup
是围绕beforeCreate
和created
生命周期钩子运行的,也就是围绕组件在创建初期时的设定工作。所以,在
setup
中不能使用this
,因为这时候还没有组件实例。
setup
的调用发生在data
、computed
或methods
被解析之前,所以无法在setup
中访问这些选项。
setup
的特性:
setup
选项在组件创建之前执行,一旦 props
被解析,就将作为组合式 API 的入口。setup
选项是一个接收 props
和 context
参数的函数。setup
函数的返回值,可以被组件的其余部分 (计算属性、方法、生命周期钩子等) ,以及组件的模板使用。setup的主要目的就是上面的第三条,你可以把这些返回值看作data、methods等的一部分。
现在让我们利用组合式API将前面例子的3个逻辑关注点聚合在一起。
首先把 setup
添加到例子中:
// src/components/UserRepositories.vue export default { components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList }, props: { user: { type: String, required: true } }, setup(props) { console.log(props) // { user: '' } return {} // 这里返回的任何内容都可以用于组件的其余部分 } // 组件的“其余部分” }
第一步,提取第一个逻辑关注点开始 (在原始代码段中标记为“1”)。
这个逻辑关注点的业务功能:从假定的外部 API 获取该用户的仓库,并在用户有任何更改时进行刷新
我们从最基础的部分开始:
// src/components/UserRepositories.vue `setup` function // 假设我们有这么一个异步的api import { fetchUserRepositories } from '@/api/repositories' // 在我们的组件内 setup (props) { let repositories = [] const getUserRepositories = async () => { repositories = await fetchUserRepositories(props.user) } return { repositories, getUserRepositories // 返回的函数与方法的行为相同,注意了,这是个函数名,不是函数的执行返回值 } }
这是我们的出发点,但它还无法生效,因为 repositories
变量目前还是非响应式的。这意味着从用户的角度来看,仓库列表将始终为空。让我们来解决这个问题!
ref
的响应式变量在 Vue 3.0 中,我们可以通过一个新的 ref
函数使任何响应式变量在任何地方起作用,如下所示:
import { ref } from 'vue' const counter = ref(0)
ref
可以接收参数,并将其包裹在一个带有 value
键的对象中返回({value: 参数值}
),然后可以使用该属性访问或更改响应式变量的值,如下所示:
import { ref } from 'vue' const counter = ref(0) console.log(counter) // { value: 0 } console.log(counter.value) // 0 counter.value++ console.log(counter.value) // 1
一定要注意这个counter的数据类型和值的获取方式!
将值封装在一个对象中,看似没有必要,但为了保持 JavaScript 中不同数据类型的行为统一,这是必须的。这是因为在 JavaScript 中,Number
或 String
等基本类型是通过值而非引用传递的:
用对象封装值使得我们可以在整个应用中安全地传递它,而不必担心在某个地方失去它的响应性。
换句话说,
ref
为我们的值创建了一个响应式引用。在整个组合式 API 中会经常使用引用的概念。
回到我们的例子,让我们创建一个响应式的 repositories
变量:
// src/components/UserRepositories.vue `setup` function import { fetchUserRepositories } from '@/api/repositories' import { ref } from 'vue' // 在我们的组件中 // 注意变化,repositories.value setup (props) { const repositories = ref([]) const getUserRepositories = async () => { repositories.value = await fetchUserRepositories(props.user) } return { repositories, getUserRepositories } }
好了!现在,每当我们调用 getUserRepositories
时,repositories
都将发生变化,视图也会更新以反映变化。我们的组件现在应该如下所示:
// src/components/UserRepositories.vue import { fetchUserRepositories } from '@/api/repositories' import { ref } from 'vue' export default { components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList }, props: { user: { type: String, required: true } }, setup (props) { const repositories = ref([]) const getUserRepositories = async () => { repositories.value = await fetchUserRepositories(props.user) } return { repositories, getUserRepositories } }, data () { return { filters: { ... }, // 3 searchQuery: '' // 2 } }, computed: { filteredRepositories () { ... }, // 3 repositoriesMatchingSearchQuery () { ... }, // 2 }, watch: { user: 'getUserRepositories' // 1 }, methods: { updateFilters () { ... }, // 3 }, mounted () { this.getUserRepositories() // 1 } }
我们已经将第一个逻辑关注点中的几个部分移到了 setup
方法中,它们彼此非常接近。
剩下的就是在 mounted
钩子中调用 getUserRepositories
,并设置一个监听器,以便在 user
发生变化时自动调用API重新获取用户仓库列表。
第二步将从生命周期钩子开始。
setup
内注册生命周期钩子为了使组合式 API 的功能和选项式 API 一样完整,我们还需要一种在 setup
中注册生命周期钩子的方法。
为此, Vue 设计了几个新函数。组合式 API 上的生命周期钩子与选项式的名称相同,但前缀为 on
:即 mounted
看起来会像 onMounted
。
这些函数接受一个回调,当钩子被组件调用时,该回调将被执行。
让我们将其添加到 setup
函数中:
// 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 } }
这样就搞定了,还是蛮简单的。
第三步,对 user
的变化做出反应。为此,我们将使用独立的 watch
函数。
watch
响应式更改就像我们在组件中使用 watch
选项并为 user
属性设置侦听器一样,我们也可以使用从 Vue 导入的 watch
函数执行相同的操作。
watch
函数接受 3 个参数:
下面让我们快速了解一下它是如何工作的
// watch函数需要提前导入 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
,侦听将触发并执行回调 (第二个参数),在本例中,它将把 'The new counter value is:5'
打印到控制台中。
以下是等效的选项式 API:
export default { 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) { // 使用 `toRefs` 创建对prop的 `user` 属性的响应式引用 const { user } = toRefs(props) const repositories = ref([]) const getUserRepositories = async () => { // 更新 `prop.user` 到 `user.value` 访问引用值 repositories.value = await fetchUserRepositories(user.value) } onMounted(getUserRepositories) // 在 user prop 的响应式引用上设置一个侦听器 watch(user, getUserRepositories) return { repositories, getUserRepositories } }
你可能已经注意到了,在 setup
的顶部我们使用了 toRefs
。这是为了确保我们的watch侦听器函数能够根据 user
属性的变化做出反应。
如果直接使用prop.user,它是非响应式的。
经过上面的一系列操作,我们就把第一个逻辑关注点聚合到了一个地方。
我们现在可以对第二个关注点执行相同的操作——基于 searchQuery
进行过滤,这次是使用计算属性。
computed
属性与 ref
和 watch
类似,也可以使用从 Vue 导入的 computed
函数在 Vue 组件外部创建计算属性。比如前面 counter 的例子:
import { ref, computed } from 'vue' const counter = ref(0) const twiceTheCounter = computed(() => counter.value * 2) counter.value++ console.log(counter.value) // 1 console.log(twiceTheCounter.value) // 2
这里我们给 computed
函数传递了第一个参数,它是一个类似 getter 的回调函数,输出的是一个只读的响应式引用。
访问新创建的计算变量twiceTheCounter
的 value,我们也需要像 ref
一样使用 .value
属性。
让我们将搜索功能移到 setup
中:
// src/components/UserRepositories.vue `setup` function import { fetchUserRepositories } from '@/api/repositories' import { ref, onMounted, watch, toRefs, computed } from 'vue' // 在我们的组件中 setup (props) { // 使用 `toRefs` 创建对 props 中的 `user` 属性 的响应式引用 const { user } = toRefs(props) const repositories = ref([]) const getUserRepositories = async () => { // 更新 `props.user ` 到 `user.value` 访问引用值 repositories.value = await fetchUserRepositories(user.value) } onMounted(getUserRepositories) // 在 user prop 的响应式引用上设置一个侦听器 watch(user, getUserRepositories) const searchQuery = ref('') // 这相当于一个data选项中的变量 // 下面的语法有多层嵌套函数。repositoriesMatchingSearchQuery最终是一个仓库列表,而不是一个函数名 const repositoriesMatchingSearchQuery = computed(() => { return repositories.value.filter( repository => repository.name.includes(searchQuery.value) ) }) return { repositories, getUserRepositories, searchQuery, repositoriesMatchingSearchQuery } }
对于其他的逻辑关注点我们也可以像上面那样做,但是你可能已经在问这个问题了——这不就是把代码移到
setup
选项并使它变得非常大吗?我们额外导入了更多的东西,编写了更多的代码,它真的比选项式API优秀吗?
记住:简单的场景用选项式API,复杂的场景用组合式API!
为了解决组合式API变得比选项式API更臃肿的问题,Vue设计了下面的模式。
将上述代码提取到一个个独立的组合式函数中,并将每个函数又保存在一个单独的js文件中。这些js文件又放在一个composables
目录中。
让我们从创建 useUserRepositories
函数开始:
// src/composables/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/composables/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 } }
现在我们有了两个单独的JS功能模块,接下来就可以开始在组件中使用它们了:
// src/components/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 { // 别看我们获取了4个引用,但不是全都需要 // 因为我们并不关心未经过滤的仓库,也就是说所有的仓库列表都是过滤后的结果 // 我们可以在 `repositories` 名称下暴露过滤后的结果 // 外部没有人使用getUserRepositories,所以它也被抛弃了 repositories: repositoriesMatchingSearchQuery, getUserRepositories, searchQuery, } }, data () { return { filters: { ... }, // 3 } }, computed: { filteredRepositories () { ... }, // 3 }, methods: { updateFilters () { ... }, // 3 } }
看见没有?解决不了问题,我就把问题本身消灭掉!你不是嫌组合式API的代码都写在组件的setup中太臃肿吗?那我把它们抽取出去,分别写在不同的JS文件中,再导入进来,美其名曰模块化!眼不见心不烦!多好,大家都高兴了!
此时,你可能已经知道了其中的奥妙,所以让我们跳到最后,迁移剩下的过滤功能。我们不需要深入了解真实代码的实现细节,因为这不是重点。
// src/components/UserRepositories.vue import { toRefs } from 'vue' import useUserRepositories from '@/composables/useUserRepositories' import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch' import useRepositoryFilters from '@/composables/useRepositoryFilters' 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 } } }
基本就是这样了。
对于初学者而言,选项式API其实更好适应,组合式API的思维模式反而更难以接受,要熟练应用更是需要转换脑子。
前面只是将setup用起来了,并没有仔细介绍setup本身。
setup
本质上是个函数,可以接收两个参数:
props
context
setup
函数中的第一个参数是 props
。它包含了组件所有的props选项中的变量。
记住,setup
函数中的 props
是响应式的,当传入新的 prop 时,它将被更新。
// MyBook.vue export default { props: { title: String }, setup(props) { console.log(props.title) } }
但是,也正因为
props
是响应式的,所以你不能使用 ES6 语法解构它,这会消除 prop 的响应性。
如果需要解构 prop,可以在 setup
函数中使用 toRefs
函数来完成此操作(前面的大例子中就是这么做的):
// MyBook.vue import { toRefs } from 'vue' setup(props) { const { title } = toRefs(props) console.log(title.value) }
如果 title
是可选的 prop,则传入的 props
中可能没有 title
。在这种情况下,toRefs
将不会为 title
创建一个 ref 。你需要使用 toRef
替代它:
// MyBook.vue import { toRef } from 'vue' setup(props) { const title = toRef(props, 'title') console.log(title.value) }
传递给 setup
函数的第二个参数是 context
。
context
是一个普通 JavaScript 对象,携带了其它可能在 setup
中有用的值,可以简单的理解为组件本身:
// MyBook.vue export default { setup(props, context) { // Attribute (非响应式对象,等同于 $attrs) console.log(context.attrs) // 插槽 (非响应式对象,等同于 $slots) console.log(context.slots) // 触发事件 (方法,等同于 $emit) console.log(context.emit) // 暴露公共 property (函数) console.log(context.expose) } }
context
是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context
使用 ES6 解构。
// MyBook.vue export default { setup(props, { attrs, slots, emit, expose }) { ... } }
attrs
和slots
是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以attrs.x
或slots.x
的方式引用属性。请注意,与props
不同,attrs
和slots
的 属性是非响应式的。如果你打算根据attrs
或slots
的更改应用副作用,那么应该在onBeforeUpdate
生命周期钩子中执行此操作。
执行 setup
时,组件实例尚未被创建。因此,你只能访问以下属性:
props
attrs
slots
emit
换句话说,你将无法访问以下组件选项:
data
computed
methods
如果 setup
返回一个对象,那么该对象的属性(键值)以及传递给 setup
的 props
参数中的属性都可以在模板中访问:
<!-- MyBook.vue --> <template> <div>{{ collectionName }}: {{ readersNumber }} {{ book.title }}</div> </template> <script> // 这个reactive是干嘛的? import { ref, reactive } from 'vue' export default { props: { collectionName: String }, setup(props) { const readersNumber = ref(0) const book = reactive({ title: 'Vue 3 Guide' }) // 暴露给 template return { readersNumber, book } } } </script>
注意,从 setup
返回的 refs 在模板中访问时是被自动浅解包的,因此不需要在模板中使用 .value
。
setup
还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态:
// MyBook.vue import { h, ref, reactive } from 'vue' export default { setup() { const readersNumber = ref(0) const book = reactive({ title: 'Vue 3 Guide' }) // 请注意这里我们需要显式使用 ref 的 value return () => h('div', [readersNumber.value, book.title]) } }
返回一个渲染函数将阻止我们返回任何其它的东西。
从内部来说这不应该成为一个问题,但当我们想要将这个组件的方法通过模板 ref 暴露给父组件时就不一样了。
我们可以通过调用 expose
来解决这个问题,给它传递一个对象,其中定义的属性将可以被外部组件实例访问:
import { h, ref } from 'vue' export default { setup(props, { expose }) { const count = ref(0) const increment = () => ++count.value expose({ increment }) return () => h('div', count.value) } }
increment
方法现在将可以通过父组件的模板 ref 访问。
this
在 setup()
内部,this
不是当前实例的引用,因为 setup()
是在解析其它组件选项之前被调用的,所以 setup()
内部的 this
的行为与其它选项中的 this
完全不同。这使得 setup()
在和其它选项式 API 一起使用时可能会导致混淆。
我们可以通过在生命周期钩子前面加上 “on” 来访问setup内的生命周期钩子函数,如下表所示。
选项式 API中的生命周期钩子 | setup 中的钩子函数 |
---|---|
beforeCreate |
不需要 |
created |
不需要 |
beforeMount |
onBeforeMount |
mounted |
onMounted |
beforeUpdate |
onBeforeUpdate |
updated |
onUpdated |
beforeUnmount |
onBeforeUnmount |
unmounted |
onUnmounted |
errorCaptured |
onErrorCaptured |
renderTracked |
onRenderTracked |
renderTriggered |
onRenderTriggered |
activated |
onActivated |
deactivated |
onDeactivated |
因为
setup
是围绕beforeCreate
和created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在setup
函数中编写。
这些钩子函数接受一个回调函数,当钩子被组件调用时将会被执行:
// MyBook.vue export default { setup() { // mounted onMounted(() => { console.log('Component is mounted!') }) } }
我们也可以在组合式 API 中使用 provide/inject。但两者都只能在当前活动实例的 setup()
期间调用。
假设我们要重写以下代码,其中包含一个 MyMap
组件,该组件使用组合式 API 为 MyMarker
组件提供用户的位置。
<!-- src/components/MyMap.vue --> <template> <MyMarker /> </template> <script> import MyMarker from './MyMarker.vue' export default { components: { MyMarker }, provide: { location: 'North Pole', geolocation: { longitude: 90, latitude: 135 } } } </script>
MyMarker
组件如下所示:
<!-- src/components/MyMarker.vue --> <script> export default { inject: ['location', 'geolocation'] } </script>
在 setup()
中使用 provide
时,我们首先需要从 vue
显式导入 provide
方法。
provide
方法允许你通过两个参数定义属性:
<String>
类型)在 MyMap
组件中,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
时,也需要从 vue
显式导入。
inject
函数有两个参数:
在 MyMarker
组件中,可以使用以下代码对其进行重构:
<!-- 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 值时使用 ref
或 reactive
。
在 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) } } </script>
现在,如果这两个属性发生变化,MyMarker
组件也将自动更新!
当使用响应式 provide / inject 值时,建议尽可能将对响应式属性的所有修改限制在定义 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 一个方法来负责改变响应式 property。
<!-- 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('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
的组件更改,我们建议对提供者的属性使用 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 时,响应式引用和模板引用的概念是统一的。
为了获得对模板内元素或组件实例的引用,我们可以像往常一样声明 ref 并从 setup() 返回:
<template> <div ref="root">This is a root element</div> </template> <script> import { ref, onMounted } from 'vue' export default { setup() { const root = ref(null) onMounted(() => { // DOM 元素将在初始渲染后分配给 ref console.log(root.value) // <div>This is a root element</div> }) return { root } } } </script>
这里我们在渲染上下文中暴露 root
,并通过 ref="root"
,将其绑定到 div 作为其 ref。
作为模板使用的 ref 的行为与任何其他 ref 一样:它们是响应式的,可以传递到 (或从中返回) 复合函数中。
export default { setup() { const root = ref(null) return () => h('div', { ref: root }) // with JSX return () => <div ref={root} /> } }
v-for
中的用法组合式 API 模板引用在 v-for
内部使用时没有特殊处理。
相反,请使用函数引用执行自定义处理:
<template> <div v-for="(item, i) in list" :ref="el => { if (el) divs[i] = el }"> {{ item }} </div> </template> <script> import { ref, reactive, onBeforeUpdate } from 'vue' export default { setup() { const list = reactive([1, 2, 3]) const divs = ref([]) // 确保在每次更新之前重置ref onBeforeUpdate(() => { divs.value = [] }) return { list, divs } } } </script>
侦听模板引用的变更可以替代前面例子中演示使用的生命周期钩子。
但与生命周期钩子的一个关键区别是,watch()
和 watchEffect()
在 DOM 挂载或更新之前运行副作用,所以当侦听器运行时,模板引用还未被更新。
<template> <div ref="root">This is a root element</div> </template> <script> import { ref, watchEffect } from 'vue' export default { setup() { const root = ref(null) watchEffect(() => { // 这个副作用在 DOM 更新之前运行,因此,模板引用还没有持有对元素的引用。 console.log(root.value) // => null }) return { root } } } </script>
因此,使用模板引用的侦听器应该用 flush: 'post'
选项来定义,这将在 DOM 更新后运行副作用,确保模板引用与 DOM 保持同步,并引用正确的元素。
<template> <div ref="root">This is a root element</div> </template> <script> import { ref, watchEffect } from 'vue' export default { setup() { const root = ref(null) watchEffect(() => { console.log(root.value) // => <div>This is a root element</div> }, { flush: 'post' }) return { root } } } </script>