hook最开始是react函数式提出并引用的概念,react函数式组件(FC)和hook的推出让react的写法更加灵活简便,个人认为vue的新版本vue3在很多方便都借鉴了react函数式组件的优点,当然也就包括了hook。
react有很多内置的hook,比如最常用的useCallback
, useMemo
,useState
,useEffect
,以及相对不常用的useReducer
等等,vue3新推出了组合式API
概念,不过在我看来,实际上的使用和设计理念和react的hook是有很多相似之处的。
react通过useEffect
来实现类组件中才有的声明周期方法来控制组件的生命周期,通过useState
或者useReducer
来创建和操作组件内状态,这里不得不说useEffect
这个钩子的秒处,他可以通过不同的用法操作处理各种副作用,实现控制react函数式组件不同的生命周期(函数式组件本没有生命周期概念,这里这样讲是为了类比类组件,更好理解)。
vue通过组合式Api来实现对vue组件的生命周期以及组件内状态的控制,比如使用ref
, reactive
来管理vue的组件内状态,使用computed
, watch
, watchEffect
来监听状态变化,使用onMounted
, onBeforeMount
, onBeforeUpdate
, onUnmounted
等来在组件的各个生命周期插入操作。
setup()
或
的代码一次。这使得代码更符合日常 JavaScript 的直觉,不需要担心闭包变量的问题。组合式 API 也并不限制调用顺序,还可以有条件地进行调用。虽然个人更喜欢react这个框架,但是这里必须承认vue上述优点是客观存在的,react函数式组件中useCallback
, useMemo
等钩子需要我们手动填入依赖项来追踪依赖,当然这依靠插件的提示是可以很好地完成依赖追踪的,但总的说来确实不如vue自动收集计算=属性和侦听器的依赖来得那么方便。
既然hook这么好用,当然有时候需要自己根据自己的业务需求来封装自己的hook来时先代码复用,这点我认为vue和react差距不大,都很好用。
hook的本质就是函数,普通函数不同的是hook的具有状态的,hook函数内部是可以调用其他的hook的
比如现在我们有个简单的需求,整个系统的loading状态存在store(vuex创建的store)里面,我们需要用这个loading状态以及改变store的方法,然而每次都直接从store里面取状态和操作状态是十分不方便的,而我们之前也提到hook是有状态的,我们可以封装hook来实现代码的简单复用:
import { reactive, toRef } from "vue";
import { useStore } from "vuex";
export default function useLoading() {
const store = useStore();
const loadingData = reactive({
loading: false,
changeLoading(value) {
store.commit("setLoading", value);
},
});
loadingData.loading = toRef(store.state, "contentLoading");
return loadingData;
}
这样useLoading
这个hook的简单封装就已经完成了,可以看到。在这个hook中我们也使用了useStore
这个外部hook和 toRef
这个组合式API,使用:
const { loading, changeLoading } = useLoading();
直接在组件中引入并且使用即可。
react自定义hook同样简单,比如现在有需求,要求在组件中判断用户权限,一般地,这种需要判断权限的地方很多,我们如果每次从store(redux创建的)去取比较麻烦,封装为hook更加方便:
import { useSelector } from "react-redux";
import { GENERAL_ADMIN, GROUP_OBSERVE_ADMIN, OBSERVE_ADMIN, TENANT_ADMIN } from "../models/user.model";
import { selectAuthority } from "../store/selectors";
export function useObSelector() {
const auth = useSelector(selectAuthority);
return [OBSERVE_ADMIN, GROUP_OBSERVE_ADMIN].includes(auth);
}
export function useTenantSelector() {
const auth = useSelector(selectAuthority);
return auth === TENANT_ADMIN;
}
export function useGeneralSelector() {
const auth = useSelector(selectAuthority);
return [GENERAL_ADMIN, TENANT_ADMIN].includes(auth);
}
使用,同样的组件中直接引入并调用即可,react自定义hook同样可以调用其他hook, 例子如下:
import * as echarts from "echarts/core";
import {
BarChart,
// 系列类型的定义后缀都为 SeriesOption
BarSeriesOption,
LineChart,
LineSeriesOption,
PieChart,
PieSeriesOption,
} from "echarts/charts";
import {
TitleComponent,
// 组件类型的定义后缀都为 ComponentOption
TitleComponentOption,
TooltipComponent,
TooltipComponentOption,
GridComponent,
GridComponentOption,
// 数据集组件
// 内置数据转换器组件 (filter, sort)
TransformComponent,
DataZoomComponent,
DataZoomComponentOption,
LegendComponentOption,
LegendComponent,
} from "echarts/components";
import { LabelLayout, UniversalTransition } from "echarts/features";
import { SVGRenderer } from "echarts/renderers";
import { useCallback, useEffect, useState } from "react";
import { selectCollapsed, selectHeight, selectWidth } from "../store/selectors";
import { useSelector } from "react-redux";
// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
export type ECOption = echarts.ComposeOption<
| LineSeriesOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| BarSeriesOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| DataZoomComponentOption
| LegendComponentOption
| PieSeriesOption
>;
// 注册必须的组件
echarts.use([TitleComponent, LegendComponent, TooltipComponent, GridComponent, TransformComponent, BarChart, LineChart, PieChart, LabelLayout, UniversalTransition, SVGRenderer, DataZoomComponent]);
export default function useChart(props: { id: string; option?: ECOption }) {
const [chart, setChart] = useState<echarts.ECharts>();
const windowWidth = useSelector(selectWidth);
const windowHeight = useSelector(selectHeight);
const collapsed = useSelector(selectCollapsed);
const initChart = useCallback(() => {
const myChart = echarts.init(document.getElementById(props.id));
if (props.option) {
myChart.setOption<ECOption>(props.option);
}
setChart(myChart);
}, [props]);
useEffect(() => {
chart?.resize && chart?.resize();
}, [windowWidth, windowHeight, chart]);
useEffect(() => {
setTimeout(() => {
chart?.resize && chart?.resize();
}, 400);
}, [collapsed, chart]);
useEffect(() => {
initChart();
}, [initChart]);
const setOption = useCallback(
(option: ECOption) => {
if (!chart || !option) {
return;
}
console.log("in");
chart.setOption(option);
},
[chart]
);
return { chart, setOption };
}
这个例子是我对echarts这个第三方库的使用封装成hook的案例,因为系统中很多的组件都会用到echarts,因此每次引入或者使用都很麻烦,特别是其他的操作,比如我们需要监听屏幕宽高的变化来调用chart实例的resize方法来重绘图表,如果在每个图表中都去注册hook去实现是很麻烦的,因此我们只需把这些方法都写在hook中,封装之后我们只需要暴露chart
实例和setOption
方法即可,使用:
import { Box } from "@mui/material";
import moment from "moment";
import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import useChart from "../../../../hooks/useChart";
import { ChartViewRange, LicenseMonthlyCount } from "../../../../models/license.model";
const elId = "subscription-bar-chart";
export default function SubscriptionBarChart(props: { data: LicenseMonthlyCount; viewRange: ChartViewRange }) {
const { t } = useTranslation();
const chart = useChart({
id: elId,
});
const option = useMemo(() => {
if (!props.data?.licenseMonthlyCount) {
return null;
}
const timeLabelFormater = props.viewRange === ChartViewRange.MONTH ? "YYYY-MM" : "YYYY-MM-DD";
const xAxisData = Object.keys(props.data?.licenseMonthlyCount).map((item) => moment(item).format(timeLabelFormater));
// const seriesData = Object.values(props.data?.licenseMonthlyCount).map((item) => item.totalNum);
const seriesData = [
{ name: t("license.total"), type: "bar", data: [], barMaxWidth: 32 },
{ name: t("license.online"), type: "bar", data: [], barMaxWidth: 32 },
{ name: t("license.offline"), type: "bar", data: [], barMaxWidth: 32 },
];
Object.values(props.data?.licenseMonthlyCount).forEach((item) => {
seriesData[0].data.push(item.totalNum);
seriesData[1].data.push(item.onlineNum);
seriesData[2].data.push(item.offlneNum);
});
return { xAxisData, seriesData };
}, [props, t]);
useEffect(() => {
chart.setOption({
tooltip: {
trigger: "axis",
},
legend: { icon: "roundRect", right: 0 },
xAxis: {
type: "category",
data: option.xAxisData,
},
yAxis: {
type: "value",
name: t("license.deviceUnit"),
nameTextStyle: {
fontWeight: "bold",
fontSize: 14,
},
},
// @ts-ignore
series: option.seriesData,
grid: {
bottom: 60,
left: "8%",
right: "8%",
top: 40,
},
dataZoom: [{ bottom: 16, height: 20 }],
});
}, [chart, option, t]);
return <Box sx={{ height: 1 }} id={elId}></Box>;
}
如代码所示,我们使用只需调用这个hook
const chart = useChart({ id: elId, option: null });
调用hook传参的时候option是可选属性我们可以传入在option也可以通过chart.setOption方法来设置option,这里我的option是通过useMemo
缓存的(类似于vue的计算属性),因此我选择使用useEffect
监听option然后动态设置option,这样基于这个hook的封装和使用都一级完成了。
hook的推出让react函数式组件大放异彩,同时vue3也对这个概念有所借鉴,个人而言,react函数式组件和hook的语法自己更加喜欢,然而上面说说的vue3的组合式API和hook的优点都是客观存在的,因此两者都是非常优秀的设计,都值得大家学习和使用。