# Vue高阶组件HOC
[toc]
HOC在React中比较流行,高阶组件在Vue中可以认为是一个接收组件为参数的函数,同时抽象出通用逻辑,再返回一个新的组件。
在 React 中
在 React 里,组件是 Class
,所以高阶组件有时候会用 装饰器
语法来实现,因为 装饰器
的本质也是接受一个 Class
返回一个新的 Class
。
在 React 的世界里,高阶组件就是 f(Class) -> 新的Class
。
在 Vue 中
在 Vue 的世界里,组件是一个对象,所以高阶组件就是一个函数接受一个对象,返回一个新的包装好的对象。
类比到 Vue 的世界里,高阶组件就是 f(object) -> 新的object
。
# 智能组件和木偶组件
木偶
组件: 就像一个牵线木偶一样,只根据外部传入的 props
去渲染相应的视图,而不管这个数据是从哪里来的。
智能
组件: 一般包在 木偶
组件的外部,通过请求等方式获取到数据,传入给 木偶
组件,控制它的渲染。
<智能组件>
<木偶组件 />
</智能组件>
它们还有另一个别名,就是 容器组件
和 ui组件
。
# 查看一个异步请求数据的案例
<template>
<div>
<p v-if="error">Error: {{error.message}}</p>
<p v-else-if="isLoading && isDelayElapsed">Loading...</p>
<ul v-else-if="!isLoading">
<li v-for="user in data"
:key="user.id">{{ user.name }}</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
// 模拟后端接口
const getUsers = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([{ name: 'A' }, { name: 'B' }]);
}, 1000);
});
};
export default defineComponent({
name: 'Users',
// props: {},
// emits: {},
// components: {},
setup(props, context) {
console.log('setup');
const error = ref(null);
const isLoading = ref(false);
const isDelayElapsed = ref(false);
const data = ref([]);
const fetchUser = () => {
error.value = null;
isLoading.value = true;
isDelayElapsed.value = false;
getUsers()
.then((res) => {
data.value = res;
})
.catch((error) => {
error.value = error;
})
.finally(() => {
isLoading.value = false;
});
setTimeout(() => {
// 200 毫秒之后才出现loading状态, 防止接口返回过快, loading闪烁
isDelayElapsed.value = true;
}, 200);
};
onMounted(() => {
fetchUser();
});
return { error, isLoading, isDelayElapsed, data };
}
});
</script>
<style scoped>
</style>
一般我们都这样写,平常也没感觉有啥问题,但是其实我们每次在写异步请求的时候都要有 loading
、 error
状态,都需要有 取数据
的逻辑,并且要管理这些状态。
那么想个办法抽象它?好像特别好的办法也不多,React 社区在 Hook 流行之前,经常用 HOC
(high order component) 也就是高阶组件来处理这样的抽象。
# 实现一个HOC的思路
下面是一个智能组件的实现思路
usePromise.js
import { defineComponent, ref, h, toRaw, onMounted } from 'vue';
// 智能组件
const usePromise = (Component, PromiseFn) => {
return defineComponent({
name: 'UsePromise',
inheritAttrs: false,
setup(props, context) {
const error = ref(null);
const isLoading = ref(false);
const isDelayElapsed = ref(false);
const data = ref([]);
const ComponentRef = ref(null);
// onMounted(() => {});
const handleGetData = params => {
error.value = null;
isLoading.value = true;
isDelayElapsed.value = false;
PromiseFn(params)
.then(res => {
data.value = res;
})
.catch(error => {
error.value = error;
})
.finally(() => {
isLoading.value = false;
});
setTimeout(() => {
// 200 毫秒之后才出现loading状态, 防止接口返回过快, loading闪烁
isDelayElapsed.value = true;
}, 200);
};
return () =>
h(
'div',
{
class: ['use-promise__wrapper']
},
[
error.value ? h('p', {}, 'Error: ' + error.message) : null,
isLoading.value && isDelayElapsed.value ? h('p', {}, 'Loading...') : null,
h(Component, {
ref: ComponentRef,
...toRaw(props),
...toRaw(context.attrs),
data: data.value,
onGetData: handleGetData
})
]
);
}
});
};
export default usePromise;
就是把异步请求和一些loading状态抽象出来。
实现属性和事件透传,木偶组件通过触发智能组件绑定的函数传递参数给api调用。
组件的使用
<script lang="ts">
import { defineComponent, h, onMounted } from 'vue';
// import Users from './Users.vue';
import usePromise from './usePromise.js';
// 模拟后端接口
const getUsers = (params = []) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([{ name: 'A' }, { name: 'B' }, ...params]);
}, 1000);
});
};
// 木偶组件,只用于显示内容
const ListCompnent = defineComponent({
name: 'ListCompnent',
// inheritAttrs: false,
emits: ['getData', 'itemClick'],
props: {
data: {
type: Array,
default: () => []
}
},
setup(props, context) {
const { slots, emit, attrs } = context;
onMounted(() => {
emit('getData', [{ name: 'C' }]);
});
return () =>
h(
'ul',
{
class: ['ul-wrapper']
},
props.data.map((item) => {
return h(
'li',
{
key: item.name,
'data-name': item.name,
onClick: () => {
emit('itemClick', item.name);
}
},
item.name
);
})
);
}
});
const ListCompnentHoc = usePromise(ListCompnent, getUsers);
export default defineComponent({
name: 'HOC',
// props: {},
// emits: {},
// components: {},
setup(props, context) {
return () =>
h('div', {}, [
h('h1', 'hoc'),
h(ListCompnentHoc, {
onItemClick: (a) => {
console.debug('onItemClick', a);
}
})
]);
}
});
</script>
<style scoped>
</style>
这只是一个思路,遇到一些常用的业务场景可以这样子抽象, 但是也不是说抽象就一定是ok的。
# 避免不必要的组件抽象
有些时候我们会去创建无渲染组件 (opens new window)或高阶组件 (用来渲染具有额外 props 的其他组件) 来实现更好的抽象或代码组织。虽然这并没有什么问题,但请记住,组件实例比普通 DOM 节点要昂贵得多,而且为了逻辑抽象创建太多组件实例将会导致性能损失。
需要提醒的是,只减少几个组件实例对于性能不会有明显的改善,所以如果一个用于抽象的组件在应用中只会渲染几次,就不用操心去优化它了。考虑这种优化的最佳场景还是在大型列表中。想象一下一个有 100 项的列表,每项的组件都包含许多子组件。在这里去掉一个不必要的组件抽象,可能会减少数百个组件实例的无谓性能消耗。
← Vue3响应式基础 Vue3 渲染可拖拽树 →