什么是组合式函数
官方文档太啰嗦,从react中学到一个词叫”可复用性逻辑抽离”,与之一个意思。
官方案例
鼠标跟随案例
首先准备一个鼠标跟随的组件
<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>
下面开始进行逻辑抽离
页面
<script setup>
import useMouse from './mouse.js'
const { x, y } = useMouse()
</script>
<style>
body{
background:black;
color:white;
}
</style>
<template>
Mouse position is at: {{ x }}, {{ y }}
</template>
逻辑抽离
import { ref,onMounted,onUnmounted } from 'vue';
export default function useMounse(){
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.addEventListener('mousemove',update);
})
return {
x,
y
}
}
再对useMounse再度抽离
import { ref } from 'vue';
import useEvent from './useEvent';
export default function useMounse(){
const x=ref(0);
const y=ref(0);
function update(event){
x.value=event.pageX;
y.value=event.pageY;
}
// 超级提取
useEvent(window,'mousemove',update);
return {
x,
y
}
}
import { onMounted,onUnmounted } from 'vue';
export default function useEvent(element,myevent,callback){
onMounted(()=>{
element.addEventListener('myevent',callback);
})
onUnmounted(()=>{
element.removeEventListener('myevent',callback)
})
}
总而言之,言而总之,好像在写react hook。
异步状态案例
<script setup>
import {} from 'vue';
const data=ref(null);
const error =ref(null);
fetch('...')
.then(()=>res.json())
.then(json=>data.value=json)
.then(err=>err.value=err)
</script>
<template>
<div v-if="error">{{error.message}}</div>
<div v-else-if="data">
Data loaded:
<pre>{{data}}</pre>
</div>
<div v-else>Loading...</div>
</template>
开始逻辑抽离
import { ref } from 'vue';
epxort function useFetch(url){
const data=ref(null);
const error=ref(null);
fetch(url)
.then(res=>res.json())
.then(josn=>json.value=json)
.then(err=>err.value=err)
}
<script setup>
import { useFetch } from './fetch.js';
const { data,error }=useFetch('...');
</script>
上述优化案例只执行一次,如果我们让他每次url发生变化然后进行重新请求,该如何去做呢?
import { ref,isRef,unref,watchEffect } from 'vue';
export function useFetch(url){
const data=ref(null);
const error=ref(null);
function doFetch(){
// 在请求之前重设状态...
data.value = null
error.value = null
// unref() 解包可能为 ref 的值
fetch(unref(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
if(isRef(url)){
// 若输入的URL是一个ref,那么启动一个响应式请求
watchEffect(doFetch)
}else{
// 否则只请求一次
// 避免监听器额外的开销
doFetch()
}
return { data,error }
}
这里是一个🎤升级版的 useFetch()
。
拓展阅读:响应式 API:工具函数 | Vue.js (vuejs.org)
约定和最佳实践
命名
use开头的驼峰命名方法
输入参数
尽管其响应性不依赖ref,组合函数仍可接收ref参数,如果编写组合式函数被其他开发者使用,最好对输入参数做兼容处理。 unref()
import { unref } from 'vue';
function useFeature(maybeRef){
// 若 maybeRef 确实是一个 ref,它的 .value 会被返回
// 否则,maybeRef 会被原样返回
const value=unref(maybeRef)
}
如果你的组合式函数在接收 ref 为参数时会产生响应式 effect,请确保使用 watch()
显式地监听此 ref,或者在 watchEffect()
中调用 unref()
来进行正确的追踪。
返回值
官方约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性:
// x 和 y 是两个 ref
const { x, y } = useMouse()
从组合式函数返回一个响应式对象会导致在对象解构过程中丢失与组合式函数内状态的响应性连接。与之相反,ref 则可以维持这一响应性连接。
副作用
在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则:
- 如果你的应用用到了服务端渲染 (SSR),请确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用,例如:
onMounted()
。这些钩子仅会在浏览器中被调用,因此可以确保能访问到 DOM。 - 确保在
onUnmounted()
时清理副作用。举例来说,如果一个组合式函数设置了一个事件监听器,它就应该在onUnmounted()
中被移除 (就像我们在useMouse()
示例中看到的一样)。当然也可以像之前的useEventListener()
示例那样,使用一个组合式函数来自动帮你做这些事。
使用限制
组合式函数在 <script setup>
或 setup()
钩子中,应始终被同步地调用。在某些场景下,你也可以在像 onMounted()
这样的生命周期钩子中使用他们。
这个限制是为了让 Vue 能够确定当前正在被执行的到底是哪个组件实例,只有能确认当前组件实例,才能够:
- 将生命周期钩子注册到该组件实例上
- 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。
在选项式 API 中使用组合式函数
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)
}
// ...其他选项
}