函数式组件中的“生命周期”
我们知道,Function Component 不同于 Class Component,它并没有生命周期的概念,而是以状态的更改来驱动代码逻辑、UI渲染的机制
对于 Function Component 来说,状态的更改到页面渲染只需要三步:
props、state的输入更改- 执行与 props、state 相关的逻辑,在
useEffect、useLayoutEffect中记录副作用 - 输出到 UI
其中,特别是我们经常会在 useEffect 里面执行像数据请求、定时器记录等逻辑,将他们放在了类似于 Vue 中的 computed 生命周期函数的位置上。
事实上,虽然我们可以通过 useEffect、useLayoutEffect 去实现 Class Component 的生命周期函数。比如 useEffect 接受的 callback 将会在 DOM 更新完毕之后调用 callback,这就完成了 Class Component 的 Mounting 阶段;当我们在 useEffect 里面返回一个 clear Function 时,会在组件卸载时执行该函数清理副作用,这样就实现了 Class Component 的 UnMounting 阶段。
然而,我们回归到本质上来:Function Component 是以状态的更改来驱动逻辑并输出到 UI 的,我个人认为不能把 useEffect 这样的 hook 当作生命周期来使用。对此,ahooks 基于 useEffect、useLayoutEffect 做了一些封装,让我们更清除代码的执行时机,更符合语义化
useMount
useMount 只在组件初始化时执行,我们来看看它的源码
import { useEffect } from 'react';
import { type EffectCallback } from 'react';
import { isFunction } from '../utils';
import isDev from '../utils/isDev';
type MountCallback = EffectCallback | (() => Promise<void | (() => void)>);
const useMount = (fn: MountCallback) => {
useEffect(() => {
const result = fn?.();
// If fn returns a Promise, don't return it as cleanup function
if (result && typeof result === 'object' && typeof (result as any).then === 'function') {
return;
}
return result as ReturnType<EffectCallback>;
}, []);
};
export default useMount;
很简单,对 useEffect 做了个封装而已,useEffect 的依赖项为空数组
需要注意的是,如果 effect 返回的是个 promise,则 promise 不能作为 cleanup 函数。 因为如果 cleanup 是异步函数,清除副作用时,可能会出现意想不到的bug
useUnMount
useUnMount在组件卸载时执行,我们可以传入一个函数 fn,当组件卸载时会去执行这个 fn 函数清除副作用。我们来看看它的源码
import { useEffect } from 'react';
import useLatest from '../useLatest';
import { isFunction } from '../utils';
import isDev from '../utils/isDev';
const useUnmount = (fn: () => void) => {
const fnRef = useLatest(fn);
useEffect(
() => () => {
fnRef.current();
},
[],
);
};
export default useUnmount;
它通过 useLatest 记录最新的 fn,内部通过 useEffect 返回 clear function,当组件卸载时,就会执行 clear function 内部的 fnRef.current()来清除副作用
代码也挺简单的
useUnmountRef
useUnmountRef 用于判断当前的组件是否已经卸载
const useUnmountedRef = () => {
const unmountedRef = useRef(false);
useEffect(() => {
unmountedRef.current = false;
return () => {
unmountedRef.current = true;
};
}, []);
return unmountedRef;
};
export default useUnmountedRef;
它就是通过 useEffect 包装了一层,然后在返回的 clear function 中修改 unmountRef.current = true 表明当前组件已经卸载,然后返回 unmountRef
结合例子来看的话
import { useBoolean, useUnmountedRef } from 'ahooks';
import { message } from 'antd';
import React, { useEffect } from 'react';
const MyComponent = () => {
const unmountedRef = useUnmountedRef();
useEffect(() => {
if (!unmountRef.current) {
message.info('MyComponent is alive')
} else {
message.error('MyComponent is unMount')
}
}, []);
return <p>Hello World!</p>;
};
export default () => {
const [state, { toggle }] = useBoolean(true);
return (
<>
<button type="button" onClick={toggle}>
{state ? 'unmount' : 'mount'}
</button>
{state && <MyComponent />}
</>
);
};
当我点击按钮时,state 为 false,不展示 MyComponent 组件,也就是卸载了组件,此时会弹出消息 MyComponent is Unmount
useUpdateEffect、useUpdateLayoutEffect
useUpdateEffect、useUpdateLayoutEffect 和 useEffect、useLayoutEffect 用法一样,只不过他们会忽略首次执行,只在依赖改变时执行
useUpdateEffect、useUpdateLayoutEffect 源码会调用 createUpdateEffect 函数,我们来看看它的源码
import { useEffect } from 'react';
import { createUpdateEffect } from '../createUpdateEffect';
export default createUpdateEffect(useEffect);
export default createUpdateEffect(useLayoutEffect);
type EffectHookType = typeof useEffect | typeof useLayoutEffect;
export const createUpdateEffect: (hook: EffectHookType) => EffectHookType =
(hook) => (callback, deps) => {
const isMounted = useRef(false);
// for react-refresh
hook(() => {
return () => {
isMounted.current = false;
};
}, []);
hook(() => {
if (!isMounted.current) {
//忽略首次执行
isMounted.current = true;
} else {
return callback();
}
}, deps);
};
export default createUpdateEffect;
它的逻辑是,首先给 isMount.current 设置为 false,然后执行对应的 hook 函数(也就是 useEffect、useLayoutEffect),然后在首次执行时,判断 !isMount.current 就不执行 callback 回调函数,把 isMount.current 设置为 true,然后当依赖 deps 发生改变时,就可以去执行 callback 回调了。这样就实现了忽略首次执行,依赖改变时才执行的逻辑
个人认为也可以直接写这样
import { useEffect, useLayoutEffect, useRef } from 'react';
type EffectHookType = typeof useEffect | typeof useLayoutEffect;
export const createUpdateEffect: (hook: EffectHookType) => EffectHookType =
(hook) => (effect, deps) => {
const isMounted = useRef(false)
hook(() => {
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
return () => {
isMounted.current = false;
};
}, deps);
};
useDeepCompareEffect、useDeepCompareLayoutEffect
同样,useDeepCompareEffect、useDeepCompareLayoutEffect 用法与 useEffect、useLayoutEffect 一致,但会通过 lodash isEqual 方法对 deps 进行比较看依赖有没有发生变化。
const depsEqual = (aDeps: DependencyList, bDeps: DependencyList = []) => {
return isEqual(aDeps, bDeps);
};
type EffectHookType = typeof useEffect | typeof useLayoutEffect;
type CreateUpdateEffect = (hook: EffectHookType) => EffectHookType;
const useDeepCompareEffect = CreateUpdateEffect = (hook) => (callback, deps) => {
// 通过 useRef 保存上一次的依赖的值
const ref = useRef<DependencyList>(undefined);
const signalRef = useRef<number>(0);
// 判断最新的依赖和旧的区别
// 如果不相等或者 deps 为 undefined,则变更 signalRef.current,从而触发 useEffect 中的回调
if (deps === undefined || !depsEqual(deps, ref.current)) {
ref.current = deps;
signalRef.current += 1;
}
hook(callback, [signalRef.current]);
};
它的思路也比较简单,首先用 ref.current 会记录旧的依赖,然后会通过 lodash 的 isEqual 方法深比较新旧依赖,如果依赖发生了改变,会将新的依赖记录在 ref.current 上,然后 signalRef.current++,这样 hook(也就是 useEffect)的依赖就发生了改变,重新执行 callback
同理 useDeepCompareLayoutEffect
useUpdate
useUpdate 会返回一个函数,调用该函数会强制组件重新渲染。我们来看看它的源码
import { useCallback, useState } from 'react';
const useUpdate = () => {
const [, setState] = useState({});
return useCallback(() => setState({}), []);
};
export default useUpdate;
很简单,就是当我们调用返回的函数时,useUpdate 内部会通过 setState 更改状态,然后就促使组件重新渲染
拿 ahooks 官网的举的例子来看:
import React from 'react';
import { useUpdate } from 'ahooks';
export default () => {
const update = useUpdate();
return (
<>
<div>Time: {Date.now()}</div>
<button type="button" onClick={update} style={{ marginTop: 8 }}>
update
</button>
</>
);
};
每当点击按钮后,执行 update 回调,组件重新刷新,Date.now() 展示最新的当前时间
总结
函数式组件是通过 状态更改驱动逻辑输出到 UI 的特性,因此我们在使用 useEffect 这样的 hook 时,不要把他放在生命周期的位置上,而是始终将他看作是以依赖状态为准则的抽象逻辑。
ahooks 通过封装了以上的 hook,使得我们在编写代码的时候,更加清楚的知道代码的执行顺序、执行时机,也更加加深我们对 React 函数组件的了解
使用这些 hook,可以让我们的代码更加具有可读性以及逻辑更加清晰