函数式组件中的“生命周期”
我们知道,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 做了一些封装,让我们更清除代码的执行时机,更符合语义化
LifeCycle 相关的 hook
useMount
useMount
只在组件初始化时
执行,我们来看看它的源码
const useMount = (fn: () => void) => {
useEffect(() => {
fn?.();
}, []);
};
export default useMount;
很简单,对 useEffect 做了个封装而已,useEffect 的依赖项为空数组
useUnMount
useUnMount
在组件卸载时
执行,我们可以传入一个函数 fn
,当组件卸载时会去执行这个 fn
函数清除副作用。我们来看看它的源码
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 () => {
//组件卸载时,会记录 unmountRef.current = false;
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
控制执行时机的 hook
useUpdateEffect、useUpdateLayoutEffect
useUpdateEffect
、useUpdateLayoutEffect
和 useEffect、useLayoutEffect 用法一样,只不过他们会忽略首次执行
,只在依赖改变时执行
useUpdateEffect、useUpdateLayoutEffect 源码会调用 createUpdateEffect
函数,我们来看看它的源码
// useUpdateEffect.js
import { useEffect } from 'react';
import { createUpdateEffect } from '../createUpdateEffect';
export default createUpdateEffect(useEffect);
// useUpdateLayoutEffect.js
import { useLayoutEffect } from 'react';
import { createUpdateEffect } from '../createUpdateEffect';
export default createUpdateEffect(useLayoutEffect);
// createUpdateEffect.js
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
回调了。这样就实现了忽略首次执行,依赖改变时才执行的逻辑
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>();
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,可以让我们的代码更加具有可读性以及逻辑更加清晰