Appearance
组合式函数
什么是组合式函数
在Vue应用的概念中该,组合式函数(Composables)是一个利用Vue的组合式API来封装和复用有状态逻辑的函数
在构建应用时,我们建厂将一些可复用的公共逻辑抽取为一个函数,这个函数封装了无状态的逻辑,它在接收一些输入后立刻返回期望的输出.
相比之下,有状态逻辑负责管理会随时间而变化的状态.一个煎蛋的例子是跟踪当前鼠标在页面中 的位置
鼠标跟踪器示例
html
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>但是如果想要复用这个逻辑呢? 可以把这个逻辑以一个组合式函数的形式提取到外部文件中
javascript
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0)
const y = ref(0)
// 组合式函数可以随时更改其状态。
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 通过返回值暴露所管理的状态
return { x, y }
}组件中使用:
html
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>这样,核心逻辑完全一样,知识把它移动到另一个外部函数中,并返回需要暴露的状态.和在组件中一样,在组合式函数中可以使用所有的组合式API. 这个例子中useMouse函数的功能就可以在任何组件中复用了
而且,在组合式函数中可以嵌套多个组合式函数,从而将复杂的逻辑拆分成可复用的函数.这也是为什么把这种设计模式成为组合式API
举例来说,我们还可以吧事件监听的逻辑抽取出去:
javascript
// event.js
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
// 如果你想的话,
// 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}这样之前的useMouse函数就可以改造成这样:
javascript
// mouse.js
import { ref } from 'vue'
import { useEventListener } from './event'
export function useMouse() {
const x = ref(0)
const y = ref(0)
useEventListener(window, 'mousemove', (event) => {
x.value = event.pageX
y.value = event.pageY
})
return { x, y }
}异步状态示例
再做异步请求时,我们常需要处理不同的状态:加载中、加载完成、加载失败等.这些状态通常封装在同一个函数中,组合式函数可以很方便地处理异步状态
没有使用组合式函数时:
html
<script setup>
import { ref } from 'vue'
const data = ref(null)
const error = ref(null)
fetch('...')
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
</script>
<template>
<div v-if="error">Oops! Error encountered: {{ error.message }}</div>
<div v-else-if="data">
Data loaded:
<pre>{{ data }}</pre>
</div>
<div v-else>Loading...</div>
</template>然而,异步请求比如网络请求使用的很频繁,我们将其抽离成一个组合式函数
javascript
// fetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
fetch(url)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
return { data, error }
}这样我们在组件中只需要:
html
<script setup>
import { useFetch } from './fetch.js'
const { data, error } = useFetch('...')
</script>接收响应式状态
组合式函数可以接收一个响应式状态,并返回一个响应式状态.这样,当响应式状态变化时,组合式函数的返回值也会自动更新
javascript
const url = ref('/initial-url')
const { data, error } = useFetch(url)
// 这将会重新触发 fetch
url.value = '/new-url'或者接收一个getter函数
javascript
// 当 props.id 改变时重新 fetch
const { data, error } = useFetch(() => `/posts/${props.id}`)这个特性是不是想到了watchEffect?同样,我们也可以使用watchEffect和toValue来重构
javascript
// fetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const fetchData = () => {
// reset state before fetching..
data.value = null
error.value = null
fetch(toValue(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
watchEffect(() => {
fetchData()
})
return { data, error }
}toValue是一个在 3.3 版本中新增的API.它的设计目的是将ref或getter规范化为值.如果参数是ref,则返回其内部值;如果参数是getter函数,则调用函数并返回其值. 否则它会原样返回参数.工作方式类似于unref,但对函数又特殊处理
注意,toValue(url)的调用是在watchEffect内部进行的,这样当url变化时,fetchData函数会重新执行,从而重新发起请求
这样,我们改造后哦的useFetch()能接收静态字符串/ref/getter,更加灵活,watchEffect会立即运行,并且会跟踪toValue(url)期间访问的任何依赖, 当url变化时,fetchData会重新执行
约定和最佳实践
命名
组合是函数约定用驼峰命名法,并使用use作为前缀
输入参数
组合式函数可以接收ref``getter或静态字符串作为参数,需要进行处理
javascript
import { toValue } from 'vue'
function useFeature(maybeRefOrGetter) {
// 如果 maybeRefOrGetter 是一个 ref 或 getter,
// 将返回它的规范化值。
// 否则原样返回。
const value = toValue(maybeRefOrGetter)
}返回值
在之前的示例代码中,我们在组合式函数中一直使用ref()而不是reactive().约定组合式函数始终返回一个包含多个ref的普通的非响应式对象,这样对象在组件中被结构为ref时,仍能保持响应性
javascript
const x = ref(0)
const y = ref(0)
// x 和 y 是两个 ref
const { x, y } = useMouse()如果从组合式函数中返回一个响应式对象,会导致对象在结构过程中丢失与组合是函数内部状态的响应性连接,而ref则不会
如果想以对象的形式来使用组合式函数中返回的状态,可以讲返回的状态用reactive()包裹起来
javascript
const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x)html
Mouse position is at: {{ mouse.x }}, {{ mouse.y }}副作用
在组合式函数中添加副作用需要注意一下规则:
- 如果应用了服务端渲染(SSR),需确保在组件挂载后才调用的生命周期钩子中执行
DOM相关的副作用,例如onMounted.这些钩子仅会在浏览器中被调用 - 确保在
onUnmounted中清理副作用,防止内存泄漏.比如注册了时间监听器,就需要在onUnmounted中移除监听器
使用限制
组合式函数只能在<script setup>标签或setup()钩子中同步调用.这些限制很重要,因为这些事Vue用于确定当前活跃的组件实例的上下文. 访问活跃的组件实例很有必要,这样才能:
- 将声明周期钩子注册到该组件实例上
- 将计算属性和监听器注册到该组件实力上,一遍该组件在被卸载时停止监听,避免内存泄露
通过抽取组合式函数改善代码结构
抽取组合式函数不仅可以复用逻辑,也可以简化代码结构.特别是组件的复杂度增高时,使用组合式API可以基于逻辑问题讲组件的代码拆分为更小的函数
html
<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'
const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>在选项式API中使用组合式函数
选项式API中,组合是函数必须在setup()函数中调用,因为setup()函数是组件的入口,且其返回值的绑定必须在setup()中返回,一遍暴露给this及其模板
javascript
import { useMouse } from './mouse.js'
import { useFetch } from './fetch.js'
export default {
setup() {
const { x, y } = useMouse()
const { data, error } = useFetch('...')
return { x, y, data, error }
},
mounted() {
// setup() 暴露的属性可以在通过 `this` 访问到
console.log(this.x)
},
// ...其他选项
}与其他模式的比较
Vue2 中的 Mixin
定义
Mixin 是一个包含组件选项的普通 JavaScript 对象:
javascript
// myMixin.js
export const myMixin = {
data() {
return {
mixinData: '来自Mixin的数据',
}
},
methods: {
mixinMethod() {
console.log('Mixin方法被调用')
},
},
}使用
javascript
import { myMixin } from './myMixin.js'
export default {
mixins: [myMixin],
created() {
console.log(this.mixinData) // 可以访问Mixin的数据
this.mixinMethod() // 可以调用Mixin的方法
},
}特性
合并策略
当组件和Mixin包含相同选项时,Vue有特定的合并策略:
- 对于
data,methods和computed,Vue会合并它们,组件的选项会覆盖Mixin的选项。 - 对于生命周期钩子,它们会被合并成一个数组,组件和Mixin的钩子都会被调用。Mixin的先执行
全局 Mixin
可通过Vue.mixin()注册全局Mixin(慎用)
典型问题
命名冲突
// mixinA
{
data() { return { count: 0 } },
methods: { increment() { this.count++ } }
}
// mixinB
{
data() { return { count: 100 } },
methods: { increment() { this.count += 10 } }
}
// 组件
{
mixins: [mixinA, mixinB],
methods: {
print() {
this.increment() // 哪个increment会被调用?
console.log(this.count) // 输出什么?
}
}
}隐式依赖
Mixin内部可能依赖特定上下文或其它Mixin,但组件可能没有引用
难以追踪来源
当多个Mixin混入时,很难确定某个属性/方法具体来自哪个Mixin。
组合式函数的优点
- 更星期的数据来源
- 使用组合式API可以在结构变量时对变量进行重命名避免冲突
和无渲染组件的对比
通过插槽定义的无渲染组件可以实现和组合式函数相同的功能,但组合式函数不会产生额外的组件实例开销.当在整个应用中使用时,无渲染组件产生的额外示例开销会带来无法忽视的性能开销