这篇文章主要讲解vue3中的逻辑复用,顺便一提vue2中的逻辑复用。
vue2中逻辑复用使用的是mixin,相信大家都不陌生,本文中mixin部分均来自于GPT。
Mixin
什么是 Mixin
Mixin 是一个包含重复逻辑或功能的对象,这个对象可以包含 Vue 组件选项(如数据、方法、钩子函数等)。在组件中使用 Mixin 时,Mixin 的内容会合并到组件自身的定义中。
使用 Mixin
定义 Mixin
首先,定义一个 Mixin 对象:
// myMixin.js
export const myMixin = {
data() {
return {
mixinData: 'This is data from mixin'
};
},
created() {
console.log('Mixin created hook called');
},
methods: {
mixinMethod() {
console.log('Method from mixin');
}
}
};
在组件中使用 Mixin
然后,在组件中使用 mixins 选项引入这个 Mixin:
// MyComponent.vue
<template>
<div>
<p>{{ mixinData }}</p>
<button @click="mixinMethod">Call Mixin Method</button>
</div>
</template>
<script>
import { myMixin } from './myMixin';
export default {
mixins: [myMixin],
data() {
return {
componentData: 'This is data from component'
};
},
created() {
console.log('Component created hook called');
},
methods: {
componentMethod() {
console.log('Method from component');
}
}
};
</script>
合并策略
Vue 对组件和 Mixin 中的选项进行合并时,会有不同的处理策略:
- 数据(data) : 组件和 Mixin 中的
data对象会进行合并,如果有同名属性,组件的数据会覆盖 Mixin 的数据。 - 生命周期钩子: 组件和 Mixin 中的生命周期钩子函数都会执行,先执行 Mixin 的钩子,然后执行组件的钩子。
- 方法(methods) : 如果组件和 Mixin 中有同名方法,组件的方法会覆盖 Mixin 的方法。
Mixin 的优缺点
优点
- 代码复用: 可以在多个组件之间复用相同的逻辑,减少重复代码。
- 逻辑分离: 可以将组件的业务逻辑分离到独立的 Mixin 中,使组件代码更清晰和易于维护。
缺点
- 命名冲突: 如果多个 Mixin 或组件自身包含同名的属性或方法,可能会导致冲突,难以排查问题。
- 不明确的依赖关系: 组件通过 Mixin 获得的功能并不显而易见,可能会导致代码的可读性和可维护性下降。
- 难以追踪: 组件中某个功能可能来自于 Mixin,排查问题时需要了解所有混入的 Mixin,增加了复杂性。
Vue3的逻辑复用
vu3中使用组合式函数进行逻辑复用。
组合式函数
官方提供的一个场景:在做异步数据请求时,我们常常需要处理不同的状态:加载中、加载成功和加载失败。
<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>
抽取成组合试函数:
// 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 }
}
在组件里使用:
<script setup>
import { useFetch } from './fetch.js'
const { data, error } = useFetch('...')
</script>
优点
从上边的例子中,组合式函数的优点显而易见:
- 逻辑分离,代码复用
- 语义化:如果命名够规范,能清楚知道这个函数的用意
- 依赖关系明确:ESM模块化很清晰的表明了依赖关系
接收响应式状态
useFetch() 接收一个静态 URL 字符串作为输入——因此它只会执行一次 fetch 并且就此结束。如果我们想要在 URL 改变时重新 fetch 呢?为了实现这一点,我们需要将响应式状态传入组合式函数,并让它基于传入的状态来创建执行操作的侦听器。
举例来说,useFetch() 应该能够接收一个 ref:
const url = ref('/initial-url')
const { data, error } = useFetch(url)
// 这将会重新触发 fetch
url.value = '/new-url'
或者接收一个 getter 函数:
// 当 props.id 改变时重新 fetch
const { data, error } = useFetch(() => `/posts/${props.id}`)
我们可以用 watchEffect() 和 toValue() API 来重构我们现有的实现:
// 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 }
}
这个版本的 useFetch() 现在能接收静态 URL 字符串、ref 和 getter,使其更加灵活。watch effect 会立即运行,并且会跟踪 toValue(url)(前提是url是作为一个响应式数据传入的,如果只是一个普通的字符串,是不能跟踪变化的) 期间访问的任何依赖项。如果没有跟踪到依赖项 (例如 url 已经是字符串),则 effect 只会运行一次;否则,它将在跟踪到的任何依赖项更改时重新运行。
toValue
将值、refs 或 getters 规范化为值。这与 unref() 类似,不同的是此函数也会规范化 getter 函数。如果参数是一个 getter,它将会被调用并且返回它的返回值。
在 Vue 3 中,处理响应式数据和普通值时,通常需要手动判断传入的值是 ref 还是普通值,然后根据情况分别处理。toValue 函数的作用是简化这种判断逻辑,使代码更加简洁和一致。
没有 toValue 时的处理逻辑
没有 toValue 时,我们需要手动判断和处理 ref 和普通值。例如:
import { ref, isRef } from 'vue';
export default {
setup() {
const count = ref(0);
const constantValue = 42;
function printValue(value) {
if (isRef(value)) {
console.log(value.value);
} else {
console.log(value);
}
}
printValue(count); // 输出:0
printValue(constantValue); // 输出:42
return {
count,
printValue
};
}
};
在上述代码中,printValue 函数需要使用 isRef 检查传入的值是否是一个 ref,然后分别处理。这会使得代码显得冗长和重复。
使用 toValue 简化代码
toValue 提供了一种简洁的方式来统一处理 ref 和普通值。它会自动检查传入的值是否是 ref,并返回原始值:
import { ref } from 'vue';
import { toValue } from '@vueuse/core';
export default {
setup() {
const count = ref(0);
const constantValue = 42;
function printValue(value) {
console.log(toValue(value));
}
printValue(count); // 输出:0
printValue(constantValue); // 输出:42
return {
count,
printValue
};
}
};
在上述代码中,printValue 函数使用 toValue 来处理传入的参数。无论传入的是 ref 还是普通值,toValue 都能正确获取其原始值,而不需要手动判断和处理。这显著简化了代码逻辑。
toValue 的实现原理
toValue 的实现原理其实很简单,它只是检查传入的值是否是 ref,如果是则返回 .value,否则返回值本身。下面是 toValue 的简单实现:
import { isRef } from 'vue';
export function toValue(value) {
return isRef(value) ? value.value : value;
}
watchEffect()
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> 输出 0
count.value++
// -> 输出 1
与 watch 的对比
watchEffect:自动追踪依赖项,适用于简单的副作用场景。watch:需要手动指定依赖项,适用于需要精细控制依赖项或处理复杂逻辑的场景。
清除副作用
以下是一个使用 watchEffect 处理定时器并清除副作用的示例:
import { ref, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
watchEffect((onCleanup) => {
const id = setInterval(() => {
console.log(`Count is: ${count.value}`);
}, 1000);
// 注册清理函数,在副作用函数重新运行之前调用
onCleanup(() => {
clearInterval(id);
});
});
function increment() {
count.value++;
}
return {
count,
increment
};
}
};
在这个示例中:
- 每次
watchEffect运行时都会创建一个新的定时器。 - 使用
onCleanup注册一个清理函数,该函数会在下一次watchEffect重新运行之前或停止监听时调用。 - 清理函数会清除之前创建的定时器,防止内存泄漏或重复的副作用。