Ahooks 中那些控制“时机”的 Hook 都是怎么实现的?

开发 前端
在 Function Component 中,使用 useEffect/useLayoutEffect 完成了 Class Components 生命周期的职责。ahooks 也是基于这两个封装了常见的代码执行时机,使用这些 hook,可以让我们的代码更加具有可读性以及逻辑更加清晰。

本文是深入浅出 ahooks 源码系列,这个系列的目标主要有以下几点:

加深对 React hooks 的理解。

学习如何抽象自定义 hooks。构建属于自己的 React hooks 工具库。

培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。

注:本系列对 ahooks 的源码解析是基于 v3.3.13。自己 folk 了一份源码,主要是对源码做了一些解读,可见 详情[1]。

本文来探索一下 ahooks 是怎么封装 React 的一些执行“时机”的?

Function Component VS Class Component

学习类似 React 和 Vue 这种框架,对它们生命周期的掌握都是必须的,我们需要清楚的知道我们代码的执行顺序,并且在不同的阶段执行不同操作的代码,比如需要挂载完成之后才去获取 dom 的值,否则可能会获取不到相应的值。

Class Component

使用过 React 的 Class Component 的同学,就会知道其组件生命周期会分成三个状态:

  • Mounting(挂载):已插入真实 DOM
  • Updating(更新):正在被重新渲染
  • Unmounting(卸载):已移出真实 DOM

简单版如下所示:

图片

其中每个状态中还会按顺序调用不同的方法,对应的详细如下(这里不展开说):

图片

可以通过官方提供这个网站查看详情[2]

可以看到,会有非常多的生命周期方法,而且在不同的版本,生命周期方法还不同。

Function Component

到了 Function Component ,会发现没有直接提及生命周期的概念,它是更彻底的状态驱动,它只有一个状态,React 负责将状态渲染到视图中。

对于 Function Component 来说由状态到页面渲染只有三步:

  • 输入状态(prop、state)
  • 执行组件的逻辑,并在 ​useEffect/useLayoutEffect 中订阅副作用
  • 输出UI(Dom节点)

重点是第二步,React 通过 useEffect/useLayoutEffect 订阅副作用。Class Component 中的生命周期都可以通过 useEffect/useLayoutEffect 来实现。它们两个的功能非常相似,我们这里看下 useEffect。

使用 useEffect 相当于告诉 React 组件需要在渲染后执行某些操作,React 将在执行 DOM 更新之后调用它。React 保证了每次运行 useEffect 的时候,DOM 已经更新完毕。这就实现了 Class Component 中的 Mounting(挂载阶段)。

当状态发生变化的时候,它能够执行对应的逻辑、更行状态并将结果渲染到视图中,这就完成了 Class Component 中的 Updating(更新阶段)。

最后通过在 useEffect 中返回一个函数,它便可以清理副作用。它的规则是:

  • 首次渲染不会进行清理,会在下一次渲染,清除上一次的副作用。
  • 卸载阶段也会执行清除操作。

通过返回一个函数,我们就能实现 Class Component 中的 Unmounting(卸载阶段)。

基于 useEffect/useLayoutEffect,ahooks 做了一些封装,能够让你更加清晰的知道你的代码执行时机。

LifeCycle - 生命周期

useMount

只在组件初始化时执行的 Hook。useEffect 依赖假如为空,只会在组件初始化的时候执行。

// 省略部分代码
const useMount = (fn: () => void) => {
// 省略部分代码
// 单纯就在 useEffect 基础上封装了一层
useEffect(() => {
fn?.();
}, []);
};

export default useMount;

useUnmount

useUnmount,组件卸载(unmount)时执行的 Hook。

useEffect 可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,所以需要返回一个函数,这个函数会在组件卸载的时候执行。

const useUnmount = (fn: () => void) => {
const fnRef = useLatest(fn);

useEffect(
// 在组件卸载(unmount)时执行的 Hook。
// useEffect 的返回值中执行函数
() => () => {
fnRef.current();
},
[],
);
};

export default useUnmount;

useUnmountedRef

获取当前组件是否已经卸载的 Hook。

通过判断有没有执行 useEffect 中的返回值判断当前组件是否已经卸载。

// 获取当前组件是否已经卸载的 Hook。
const useUnmountedRef = () => {
const unmountedRef = useRef(false);
useEffect(() => {
unmountedRef.current = false;
// 如果已经卸载,则会执行 return 中的逻辑
return () => {
unmountedRef.current = true;
};
}, []);
return unmountedRef;
};

export default useUnmountedRef;

Effect

这里只会讲官方文档 Effect 下面的几个,有部分是定时器、防抖节流等,咱们后面的系列具体分析。

useUpdateEffect 和 useUpdateLayoutEffect

useUpdateEffect 和 useUpdateLayoutEffect 的用法跟 useEffect 和 useLayoutEffect 一样,只是会忽略首次执行,只在依赖更新时执行。

实现思路:初始化一个标识符,刚开始为 false。当首次执行完的时候,置为 true。只有标识符为 true 的时候,才执行回调函数。

// 忽略首次执行
export const createUpdateEffect: (hook: effectHookType) => effectHookType =
(hook) => (effect, deps) => {
const isMounted = useRef(false);

// for react-refresh
hook(() => {
return () => {
isMounted.current = false;
};
}, []);

hook(() => {
// 首次执行完时候,设置为 true,从而下次依赖更新的时候可以执行逻辑
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
}, deps);
};

useDeepCompareEffect和useDeepCompareLayoutEffect

用法与 useEffect 一致,但 deps 通过 lodash isEqual 进行深比较。

通过 useRef 保存上一次的依赖的值,跟当前的依赖对比(使用 lodash 的 isEqual),并将对比结果作为 useEffect 的依赖项,从而决定回调函数是否执行。

const depsEqual = (aDeps: DependencyList, bDeps: DependencyList = []) => {
return isEqual(aDeps, bDeps);
};

const useDeepCompareEffect = (effect: EffectCallback, deps: DependencyList) => {
// 通过 useRef 保存上一次的依赖的值
const ref = useRef<DependencyList>();
const signalRef = useRef<number>(0);

// 判断最新的依赖和旧的区别
// 如果相等,则变更 signalRef.current,从而触发 useEffect 中的回调
if (!depsEqual(deps, ref.current)) {
ref.current = deps;
signalRef.current += 1;
}

useEffect(effect, [signalRef.current]);
};

useUpdate

useUpdate 会返回一个函数,调用该函数会强制组件重新渲染。

返回的函数通过变更 useState 返回的 state,从而促使组件进行更新。

import { useCallback, useState } from 'react';

const useUpdate = () => {
const [, setState] = useState({});
// 通过设置一个全新的状态,促使 function 组件更新
return useCallback(() => setState({}), []);
};

export default useUpdate;

总结与思考

在我们写代码的时候需要清晰的知道,组件的生命周期是怎样的,我们代码的执行顺序、执行的时机是怎样的。

在 Function Component 中,使用 useEffect/useLayoutEffect 完成了 Class Components 生命周期的职责。ahooks 也是基于这两个封装了常见的代码执行时机,使用这些 hook,可以让我们的代码更加具有可读性以及逻辑更加清晰。

参考资料

[1]详情: https://github.com/GpingFeng/hooks

[2]详情: https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

[3]大家都能看得懂的源码(一)ahooks 整体架构篇: https://juejin.cn/post/7105396478268407815

[4]如何使用插件化机制优雅的封装你的请求hook : https://juejin.cn/post/7105733829972721677

[5]ahooks 是怎么解决 React 的闭包问题的?: https://juejin.cn/post/7106061970184339464

[6]ahooks 是怎么解决用户多次提交问题?: https://juejin.cn/post/7106461530232717326

责任编辑:武晓燕 来源: 前端杂货铺
相关推荐

2022-07-01 07:31:18

AhooksDOM场景

2022-04-14 11:50:39

函数组件hook

2014-08-22 13:45:59

2022-07-06 07:42:14

DOMHook标签

2022-06-08 08:01:20

useEffect数组函数

2020-09-18 15:08:13

5G网络技术

2022-06-28 07:41:38

useMountReactahooks

2022-07-03 23:26:38

DOMHook封装

2019-10-24 14:15:52

Python工具数据

2009-02-09 10:06:03

并发控制Web应用悲观锁

2017-07-28 10:11:56

推送APP推送推送轻架构

2022-06-09 09:20:41

ahooksaxios

2018-04-19 13:58:46

SSD闪存下跌

2010-09-10 09:51:05

DIVCSS

2018-10-24 11:29:55

科技大佬硅谷老婆

2021-03-08 08:32:42

技术HTTP请求

2010-06-02 09:33:04

2020-08-17 17:47:30

内存技术测试

2017-03-08 08:53:44

Git命令 GitHub

2021-08-11 21:46:47

MySQL索引join
点赞
收藏

51CTO技术栈公众号