在 Vue 开发中,逻辑复用是提高代码质量和开发效率的核心技巧。今天就通过对比四种主流的逻辑复用方案:Hooks(Vue3)、Mixin、HOC(高阶组件)和普通函数封装,看下有啥不一样。
场景定义
我们以一个常见的 "用户信息加载" 功能为例,该功能包含以下逻辑:
- 加载用户数据
- 管理加载状态(加载中 / 加载完成 / 加载失败)
- 提供重新加载方法
- 组件卸载时取消未完成的请求
我们将用四种不同方案实现相同功能,以便直观对比。
1. 普通函数封装
普通函数封装是最基础的逻辑复用方式,通过函数将逻辑打包,返回需要的数据和方法。
// userLoader.js
import axios from 'axios';
export function createUserLoader() {
let user = null;
let loading = false;
let error = null;
let cancelToken = null;
const loadUser = async (userId) => {
// 取消之前的请求
if (cancelToken) {
cancelToken.cancel('Previous request canceled');
}
loading = true;
error = null;
cancelToken = axios.CancelToken.source();
try {
const response = await axios.get(`/api/users/${userId}`, {
cancelToken: cancelToken.token
});
user = response.data;
return user;
} catch (err) {
if (!axios.isCancel(err)) {
error = err.message;
}
throw err;
} finally {
loading = false;
}
};
return {
user,
loading,
error,
loadUser,
cancel: () => cancelToken?.cancel('Manual cancel')
};
}
在组件中使用:
vue
<template>
<div>
<div v-if="loading">加载中...</div>
<div v-if="error" class="error">{{ error }}</div>
<div v-if="user">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
<button @click="loadUser(1)">加载用户</button>
</div>
</template>
<script>
import { createUserLoader } from './userLoader';
export default {
data() {
const userLoader = createUserLoader();
return {
user: userLoader.user,
loading: userLoader.loading,
error: userLoader.error,
loadUser: userLoader.loadUser,
cancel: userLoader.cancel,
};
},
beforeUnmount() {
this.cancel();
}
};
</script>
缺点很明显就是缺乏响应性,但是呢,代码看上去就很简单明了。
适用场景:
- 简单的工具类逻辑复用
- 非响应式数据处理
- 跨框架共享的通用逻辑
2. Mixin(混入)
Mixin 是 Vue2 中官方推荐的逻辑复用方式,允许将组件选项合并到多个组件中。
// userLoaderMixin.js
import axios from 'axios';
export default {
data() {
return {
user: null,
userLoading: false,
userError: null,
userCancelToken: null
};
},
methods: {
loadUser(userId) {
// 取消之前的请求
if (this.userCancelToken) {
this.userCancelToken.cancel('Previous request canceled');
}
this.userLoading = true;
this.userError = null;
this.userCancelToken = axios.CancelToken.source();
return axios.get(`/api/users/${userId}`, {
cancelToken: this.userCancelToken.token
})
.then(response => {
this.user = response.data;
return response.data;
})
.catch(err => {
if (!axios.isCancel(err)) {
this.userError = err.message;
}
throw err;
})
.finally(() => {
this.userLoading = false;
});
},
cancelUserLoad() {
this.userCancelToken?.cancel('Manual cancel');
}
},
beforeUnmount() {
this.cancelUserLoad();
}
};
在组件中使用:
vue
<template>
<div>
<div v-if="userLoading">加载中...</div>
<div v-if="userError" class="error">{{ userError }}</div>
<div v-if="user">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
<button @click="loadUser(1)">加载用户</button>
</div>
</template>
<script>
import userLoaderMixin from './userLoaderMixin';
export default {
mixins: [userLoaderMixin],
// 组件其他选项...
};
</script>
看上去就很简单明了,也不缺乏响应性。
但是有个缺点,如果是大的项目中去修改文件的时会有重名的问题,因为mixin是与原组件中的api是进行合并的操作,并非覆盖操作,所以一旦出现重名就会出现问题,在排查的时候也会比较难受。大的项目中要做好规范,尽量把名字啥的写成唯一的。
适用场景:
- Vue2 项目中简单的逻辑复用
- 不需要参数配置的通用逻辑
- 团队内部有明确规范的项目
3. HOC(高阶组件)
高阶组件是一个接收组件作为参数并返回新组件的函数,通过包装组件实现逻辑复用。
// withUserLoader.js
import Vue from 'vue';
import axios from 'axios';
export function withUserLoader(WrappedComponent) {
return Vue.extend({
data() {
return {
user: null,
userLoading: false,
userError: null,
userCancelToken: null
};
},
methods: {
loadUser(userId) {
// 取消之前的请求
if (this.userCancelToken) {
this.userCancelToken.cancel('Previous request canceled');
}
this.userLoading = true;
this.userError = null;
this.userCancelToken = axios.CancelToken.source();
return axios.get(`/api/users/${userId}`, {
cancelToken: this.userCancelToken.token
})
.then(response => {
this.user = response.data;
return response.data;
})
.catch(err => {
if (!axios.isCancel(err)) {
this.userError = err.message;
}
throw err;
})
.finally(() => {
this.userLoading = false;
});
},
cancelUserLoad() {
this.userCancelToken?.cancel('Manual cancel');
}
},
beforeUnmount() {
this.cancelUserLoad();
},
render(h) {
// 将状态和方法通过 props 传递给被包装组件
return h(WrappedComponent, {
props: {
...this.$props,
user: this.user,
userLoading: this.userLoading,
userError: this.userError,
loadUser: this.loadUser,
cancelUserLoad: this.cancelUserLoad
},
on: this.$listeners
});
}
});
}
在组件中使用:
vue
<template>
<div>
<div v-if="userLoading">加载中...</div>
<div v-if="userError" class="error">{{ userError }}</div>
<div v-if="user">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
<button @click="loadUser(1)">加载用户</button>
</div>
</template>
<script>
import { withUserLoader } from './withUserLoader';
export default withUserLoader({
props: ['user', 'userLoading', 'userError', 'loadUser', 'cancelUserLoad'],
// 组件其他选项...
});
</script>
这样看来其实也没啥毛病,但是真的不建议去用,首先就是太绕啦,在使用的时候还要再包裹一层组件,不建议用。
适用场景:
- 需要对组件进行增强的场景
- 逻辑相对独立,参数配置简单的情况
- 基于 props 通信的组件复用
4. Hooks(Vue3 Composition API)
Vue3 的 Hooks 基于 Composition API,通过函数封装逻辑并返回响应式数据和方法。
// useUserLoader.js
import { ref, onUnmounted } from 'vue';
import axios from 'axios';
export function useUserLoader() {
const user = ref(null);
const loading = ref(false);
const error = ref(null);
let cancelToken = null;
const loadUser = async (userId) => {
// 取消之前的请求
if (cancelToken) {
cancelToken.cancel('Previous request canceled');
}
loading.value = true;
error.value = null;
cancelToken = axios.CancelToken.source();
try {
const response = await axios.get(`/api/users/${userId}`, {
cancelToken: cancelToken.token
});
user.value = response.data;
return response.data;
} catch (err) {
if (!axios.isCancel(err)) {
error.value = err.message;
}
throw err;
} finally {
loading.value = false;
}
};
const cancelUserLoad = () => {
cancelToken?.cancel('Manual cancel');
};
// 组件卸载时取消请求
onUnmounted(cancelUserLoad);
return {
user,
loading,
error,
loadUser,
cancelUserLoad
};
}
在组件中使用:
vue
<template>
<div>
<div v-if="loading">加载中...</div>
<div v-if="error" class="error">{{ error }}</div>
<div v-if="user">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
<button @click="loadUser(1)">加载用户</button>
</div>
</template>
<script setup>
import { useUserLoader } from './useUserLoader';
// 直接调用 Hooks 获取状态和方法
const { user, loading, error, loadUser } = useUserLoader();
</script>
👏👏👏👏👏👏,看这个就很好用,简答明了,可惜就是vue项目中只有vue3能用。
综合对比表格
| 特性 | 普通函数封装 | Mixin | HOC | Hooks |
|---|---|---|---|---|
| 响应式支持 | 需手动处理 | 自动支持 | 自动支持 | 自动支持 |
| 命名冲突 | 无(显式引用) | 严重 | 无(显式传递) | 无(显式引用) |
| 逻辑来源 | 清晰 | 模糊 | 较清晰 | 清晰 |
| 参数传递 | 支持 | 不支持 | 支持 | 支持 |
| 生命周期 | 手动处理 | 自动合并 | 内部管理 | 自动关联 |
| 代码组织 | 分散 | 分散 | 包装式 | 聚合式 |
| 组合能力 | 弱 | 中 | 中 | 强 |
| 学习成本 | 低 | 低 | 中 | 中 |
| Vue2 支持 | 支持 | 原生支持 | 支持 | 需通过插件 |
| Vue3 支持 | 支持 | 支持(兼容性) | 支持 | 原生支持 |
实际项目中的选择
- 优先选择 Hooks:在 Vue3 项目中,Hooks 几乎是所有逻辑复用场景的最佳选择,它解决了其他方案的大部分痛点,提供了清晰的逻辑组织和灵活的组合能力。
- Mixin 谨慎使用:在 Vue2 项目中,Mixin 可以解决简单的复用问题,但要注意规范命名(如添加统一前缀),避免多层嵌套和命名冲突。
- HOC 适用于特定场景:当需要对组件进行增强或包装时(如权限控制、日志记录),HOC 是一个不错的选择,但要避免过度使用导致的组件层级过深。
- 普通函数封装作为补充:对于与组件逻辑无关的通用工具类逻辑,普通函数封装仍然是最简单高效的方式。
- 迁移策略:从 Vue2 迁移到 Vue3 时,可以优先将复杂的 Mixin 重构为 Hooks,提高代码可维护性。