组合式函数和普通函数的区别:
1、 组合式函数里:可以使用组件相关的特性(生命周期:onMounted,onBeforeUpdate等),命名用use开头
2、普通函数里:不能使用组件相关的特性。
hooks:钩子,钩什么东西?钩子钩的是vue组件的相关特性(如:生命周期,响应式等等)
1、 什么是“组合式函数”
在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数[也可以叫做自定义hooks(钩子)]。
当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑(跟组件的相关特性没有关系),它在接收一些输入后立刻返回所期望的输出。
相比之下,有状态逻辑(跟组件的特性有关)负责管理会随时间而变化的状态。
无状态逻辑:用的普通函数。跟组件没有关系。
有状态逻辑:用的是hooks。有状态说的就是组件的状态。在hooks里可以使用组件的相关特性(ref,reactive,生命周期钩子函数)。既就是:hooks封装的是组件的业务逻辑。
一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。
如果我们要直接在组件中使用组合式 API 实现鼠标跟踪功能,它会是这样的:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>composition_mouse</title>
</head>
<body>
<div id="app">
鼠标的位置为:({{ x }}, {{ y }})
</div>
</body>
<script src="./js/vue.global.js"></script>
<script>
const { createApp, ref, onMounted, onUnmounted } = Vue
const app = createApp({
setup () {
const x = ref(0)
const y = ref(0)
function updatePosition (event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => {
window.addEventListener('mousemove', updatePosition, false)
})
onUnmounted(() => {
window.removeEventListener('mousemove', updatePosition, false)
})
return {
x, y
}
}
})
app.mount('#app')
</script>
</html>
但是,如果我们想在多个组件中复用这个相同的逻辑呢?我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中
2、 如何定义和使用组合式函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>composition_mouse_hooks</title>
</head>
<body>
<div id="app">
鼠标的位置为:({{ x }}, {{ y }})
</div>
</body>
<script src="./js/vue.global.js"></script>
<script>
const { createApp, ref, onMounted, onUnmounted } = Vue
function useMouse () {
const x = ref(0)
const y = ref(0)
function updatePosition (event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => {
window.addEventListener('mousemove', updatePosition, false)
})
onUnmounted(() => {
window.removeEventListener('mousemove', updatePosition, false)
})
return { x, y }
}
const app = createApp({
setup () {
const { x, y } = useMouse()
return {
x, y
}
}
})
app.mount('#app')
</script>
</html>
如你所见,核心逻辑完全一致,我们做的只是把它移到一个外部函数中去,并返回需要暴露的状态。和在组件中一样,你也可以在组合式函数中使用所有的组合式API。现在,useMouse()的功能可以在任何组件中轻易复用了。
更酷的是,你还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他的组合式函数。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>composition_mouse_more_hooks</title>
</head>
<body>
<div id="app">
鼠标的位置为:({{ x }}, {{ y }})
</div>
</body>
<script src="./js/vue.global.js"></script>
<script>
const { createApp, ref, onMounted, onUnmounted } = Vue
function useEventListener (target, event, callback) {
onMounted(() => {
target.addEventListener(event, callback, false)
})
onUnmounted(() => {
target.removeEventListener(event, callback, false)
})
}
function useMouse () {
const x = ref(0)
const y = ref(0)
// function updatePosition (event) {
// x.value = event.pageX
// y.value = event.pageY
// }
useEventListener(window, 'mousemove', (event) => {
x.value = event.pageX
y.value = event.pageY
})
return { x, y }
}
const app = createApp({
setup () {
const { x, y } = useMouse()
return {
x, y
}
}
})
app.mount('#app')
</script>
</html>
3、 异步状态
在做异步数据请求时,我们常常需要处理不同的状态:加载中、加载成功和加载失败。
<script setup>
import {ref} from "vue";
import axios from "axios";
let data = ref(null);
function getData(){
axios({
url:"http://121.89.205.189:3001/api/pro/list"
})
.then(res=>{
data.value = res.data;
console.log("data",data);
})
}
getData();
</script>
<template>
<div>
<h1>异步组件加载的状态封装:</h1>
<ul v-if="data">
数据显示
<li v-for="item in data.data" :key="item.proid">
<p>商品编号:{{item.proid}}</p>
<p>商品:{{item.proname}}</p>
</li>
</ul>
<div v-else>加载中………………</div>
</div>
</template>
<style scoped>
</style>
如果在每个需要获取数据的组件中都要重复这种模式,那就太繁琐了。让我们把它抽取成一个组合式函数:
异步hooks示例:
<script setup>
import { reactive, ref } from "vue";
import axios from "axios";
function useData(url) {
let data = ref(null);
let err = ref(null);
axios({
url,
})
.then((res) => {
data.value = res.data;
console.log("data", data);
})
.catch((err) => {
err.value = err;
});
return { data, err };
}
const { data, error } = useData("http://121.89.205.189:3001/api/pro/list");
</script>
<template>
<div>
<h1>异步组件加载的状态封装:</h1>
<ul v-if="data">
数据显示
<li v-for="item in data.data" :key="item.proid">
<p>商品编号:{{ item.proid }}</p>
<p>商品名称:{{ item.proname }}</p>
</li>
</ul>
<div v-else>加载中………………</div>
</div>
</template>
<style scoped>
</style>
useData()接收一个静态的 URL 字符串作为输入,所以它只执行一次请求,然后就完成了。但如果我们想让它在每次 URL 变化时都重新请求呢?那我们可以让它同时允许接收 ref 作为参数:
接收不同url的异步hooks:
<script setup>
import { isRef, ref, watchEffect, unref, computed } from "vue";
import axios from "axios";
function useData(url) {
console.log("useData");
// 2.1、data和err都是响应式的数据
let data = ref(null);
let err = ref(null);
if (isRef(url)) {//如果url是ref的对象的话,那么就使用watchEffect。
//2.2、 定义了一个监听。当url发生变化时,就会调用回调函数。
watchEffect(() => {
console.log("发送请求获取数据");
axios({
url: unref(url), //unref() 解包可能为 ref 的值 unref(url) <===> url.value
})
.then((res) => {
data.value = res.data;
})
.catch((err) => {
err.value = err;
});
});
} else {
// 如果url不是ref的对象的话,就不用使用watchEffect,直接发送请求即可。
}
return { data, err };
}
let pageIndex = ref(1);
// 1、定义计算属性url,当pageInde的值发生变化时,url就会发生变化,url是响应式的数据。
const url = computed(
() => "http://121.89.205.189:3001/api/pro/list?count=" + pageIndex.value
);
const { data, error } = useData(url);
</script>
<template>
<div>
<h1>异步组件加载的状态封装:</h1>
<p>选择页码:{{ pageIndex }}</p>
<select v-model="pageIndex">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
<ul v-if="data">
数据显示
<li v-for="item in data.data" :key="item.proid">
<p>商品编号:{{ item.proid }}</p>
<p>商品名称:{{ item.proname }}</p>
</li>
</ul>
<div v-else>加载中………………</div>
</div>
</template>
<style scoped>
</style>
4、 约定和最佳实践
(1)、命名
组合式函数约定用驼峰命名法命名,并以“use”作为开头(是约定,不是强制)。
(2)、输入参数
尽管其响应性不依赖 ref,组合式函数仍可接收 ref 参数。如果编写的组合式函数会被其他开发者使用,你最好在处理输入参数时兼容 ref 而不只是原始的值。unref()工具函数会对此非常有帮助:
import { unref } from 'vue'
function useFeature(maybeRef) {
// 若 maybeRef 确实是一个 ref,它的 .value 会被返回
// 否则,maybeRef 会被原样返回
const value = unref(maybeRef)
}
如果你的组合式函数在接收 ref 为参数时会产生响应式 effect,请确保使用watch()显式地监听此 ref,或者在watchEffect()中调用unref()来进行正确的追踪。
(3)、返回值
你可能已经注意到了,我们一直在组合式函数中使用ref()而不是reactive()。我们推荐的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性:
// x 和 y 是两个 ref
const { x, y } = useMouse()
从组合式函数返回一个响应式对象会导致在对象解构过程中丢失与组合式函数内状态的响应性连接。与之相反,ref 则可以维持这一响应性连接。
如果你更希望以对象属性的形式来使用组合式函数中返回的状态,你可以将返回的对象用reactive()包装一次,这样其中的 ref 会被自动解包,例如:
const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x)
Mouse position is at: {{ mouse.x }}, {{ mouse.y }}
(4)、副作用
在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则:
-
如果你的应用用到了服务端渲染 (SSR),请确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用,例如:onMounted()。这些钩子仅会在浏览器中被调用,因此可以确保能访问到 DOM。
-
确保在onUnmounted()时清理副作用。举例来说,如果一个组合式函数设置了一个事件监听器,它就应该在onUnmounted()中被移除 (就像我们在useMouse()示例中看到的一样)。
(5)、使用限制
组合式函数在<script setup>或setup()钩子中,应始终被同步地调用。在某些场景下,你也可以在像onMounted()这样的生命周期钩子中使用他们。
这个限制是为了让 Vue 能够确定当前正在被执行的到底是哪个组件实例,只有能确认当前组件实例,才能够:
- 将生命周期钩子注册到该组件实例上;
- 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄露