Jacky's blog Jacky's blog
首页
  • 编码专题
  • 深入浅出 Vite
  • 深入浅出 babel
  • 快速上手API
  • 深入浅出 react
  • Node

    • code-notebook
  • 状态管理

    • redux
  • 前端工程化

    • Wepack
  • React源码

    • React源码
  • 组件库封装

    • 组件库
  • 开发工具

    • Vscode 插件
  • 项目展示
  • 案例中心 (opens new window)
  • First Project
  • 基础算法题
  • 链表题
  • 动态规划
  • 双指针
  • 递归
  • 数据结构
  • 前端学习计划 (opens new window)
  • 技术随笔
  • 转载文章
  • 包管理工具
  • 前端学习周报
  • VSCode插件
  • Promise 专题
  • 函数技巧
  • React 专题
  • 配置文件

    • TSCONFIG-配置 (opens new window)
    • NGINX-配置 (opens new window)
    • 正则规则查询手册 (opens new window)
    • Lint 配置 (opens new window)
  • 教程

    • GIT-教程
    • NPM SCRIPTS-工作流 (opens new window)
    • DOCKER-教程 (opens new window)
    • LERNA-教程 (opens new window)
    • GIT-常用操作整理 (opens new window)
  • VSCode

    • LAUNCH.JSON (opens new window)
  • 指令

    • NPM 指令 (opens new window)
    • NVM 指令 (opens new window)
    • Nginx 指令 (opens new window)
    • YARN 指令 (opens new window)
    • PNPM 指令 (opens new window)
  • 库

    • FS-EXTRA 库 (opens new window)
    • NODE 库-PATH (opens new window)
  • 永远的神

    • 魔法师卡颂-自顶向下学 React 源码 (opens new window)
    • 全栈潇晨 (opens new window)
    • 博客-程序员山月-Daily (opens new window)
    • 淘系前端:冴羽 (opens new window)
  • 系列文章

    • 《图解HTTP》 (opens new window)
    • 《ES6标准入门》 (opens new window)
    • 《现代JavaScript教程》 (opens new window)
    • 《深入浅出Webpack》 (opens new window)
    • VSCode 插件系列:小茗同学 (opens new window)
    • JEST 教程 (opens new window)
    • 前端精读周刊:各种精读系列 (opens new window)
    • 一文吃透系列 (opens new window)
    • 图解 REACT 原理 (opens new window)
  • 实用网站

    • MDN (opens new window)
    • CAN I USE (opens new window)
    • TYPESCRIPT-ESLint-RULES (opens new window)
    • ESLint-RULES (opens new window)
    • FRONT-END TREND (opens new window)
    • NPM TREND (opens new window)
    • 在线分析 Node 依赖 (opens new window)
    • FIND NPM (opens new window)
    • CODE PEN (opens new window)
    • 印记中文 (opens new window)
    • TOOL.LU (opens new window)
    • 阮一峰-网道 (opens new window)
    • DIGITAL OCEAN (opens new window)
    • DEVDOCS.IO (opens new window)
    • JOI (opens new window)
  • 算法

    • 小浩算法 (opens new window)
    • LABULADONG 的算法小抄 (opens new window)
    • 力扣 SOLUTION (opens new window)
    • HACKER RANK (opens new window)
    • 代码随想录 (opens new window)
  • 博客系列

    • 美团大佬 (opens new window)
    • 蜡笔小伟 (opens new window)
    • 优秀博客1 (opens new window)
    • 优秀博客2-umi (opens new window)
    • 优质博客 (opens new window)
  • CSS

    • CSS-EASING 库 (opens new window)
    • ROUGH.JS (opens new window)
    • CSS 网站收集
    • UNOCSS (opens new window)
  • 前端

    • PROMISE (opens new window)
    • UNDERSCORE.JS (opens new window)
    • study with BGM (opens new window)
    • nginx【B站视频】 (opens new window)
    • 机器学习
    • Js基础
  • 掘金已购课程

    • 前端自动化测试精讲 (opens new window)
    • 深入浅出 Vite (opens new window)
    • 现代 Web 布局 (opens new window)
    • 前端算法与数据结构 (opens new window)
    • 基于 Vite 的 SSG 框架开发实战 (opens new window)
    • SSR 实战:官网开发指南 (opens new window)
    • WebGL 入门与实践 (opens new window)
    • 玩转 CSS 的艺术之美 (opens new window)
    • 前端调试通关秘籍 (opens new window)
    • React 进阶实践指南 (opens new window)
    • TypeScript 全面进阶指南 (opens new window)
    • 前端缓存技术与方案解析 (opens new window)
    • npm scripts 前端工作流 (opens new window)
    • Webpack5 核心原理与应用实践 (opens new window)
  • 购物车

    • 张鑫旭-技术写作指南 (opens new window)
    • 深入剖析 Node.js 底层原理 (opens new window)
    • 前端开发者的现代 C++ 课 (opens new window)
    • 从前端到全栈 (opens new window)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Jacky Wang

行到水穷处,坐看云起时
首页
  • 编码专题
  • 深入浅出 Vite
  • 深入浅出 babel
  • 快速上手API
  • 深入浅出 react
  • Node

    • code-notebook
  • 状态管理

    • redux
  • 前端工程化

    • Wepack
  • React源码

    • React源码
  • 组件库封装

    • 组件库
  • 开发工具

    • Vscode 插件
  • 项目展示
  • 案例中心 (opens new window)
  • First Project
  • 基础算法题
  • 链表题
  • 动态规划
  • 双指针
  • 递归
  • 数据结构
  • 前端学习计划 (opens new window)
  • 技术随笔
  • 转载文章
  • 包管理工具
  • 前端学习周报
  • VSCode插件
  • Promise 专题
  • 函数技巧
  • React 专题
  • 配置文件

    • TSCONFIG-配置 (opens new window)
    • NGINX-配置 (opens new window)
    • 正则规则查询手册 (opens new window)
    • Lint 配置 (opens new window)
  • 教程

    • GIT-教程
    • NPM SCRIPTS-工作流 (opens new window)
    • DOCKER-教程 (opens new window)
    • LERNA-教程 (opens new window)
    • GIT-常用操作整理 (opens new window)
  • VSCode

    • LAUNCH.JSON (opens new window)
  • 指令

    • NPM 指令 (opens new window)
    • NVM 指令 (opens new window)
    • Nginx 指令 (opens new window)
    • YARN 指令 (opens new window)
    • PNPM 指令 (opens new window)
  • 库

    • FS-EXTRA 库 (opens new window)
    • NODE 库-PATH (opens new window)
  • 永远的神

    • 魔法师卡颂-自顶向下学 React 源码 (opens new window)
    • 全栈潇晨 (opens new window)
    • 博客-程序员山月-Daily (opens new window)
    • 淘系前端:冴羽 (opens new window)
  • 系列文章

    • 《图解HTTP》 (opens new window)
    • 《ES6标准入门》 (opens new window)
    • 《现代JavaScript教程》 (opens new window)
    • 《深入浅出Webpack》 (opens new window)
    • VSCode 插件系列:小茗同学 (opens new window)
    • JEST 教程 (opens new window)
    • 前端精读周刊:各种精读系列 (opens new window)
    • 一文吃透系列 (opens new window)
    • 图解 REACT 原理 (opens new window)
  • 实用网站

    • MDN (opens new window)
    • CAN I USE (opens new window)
    • TYPESCRIPT-ESLint-RULES (opens new window)
    • ESLint-RULES (opens new window)
    • FRONT-END TREND (opens new window)
    • NPM TREND (opens new window)
    • 在线分析 Node 依赖 (opens new window)
    • FIND NPM (opens new window)
    • CODE PEN (opens new window)
    • 印记中文 (opens new window)
    • TOOL.LU (opens new window)
    • 阮一峰-网道 (opens new window)
    • DIGITAL OCEAN (opens new window)
    • DEVDOCS.IO (opens new window)
    • JOI (opens new window)
  • 算法

    • 小浩算法 (opens new window)
    • LABULADONG 的算法小抄 (opens new window)
    • 力扣 SOLUTION (opens new window)
    • HACKER RANK (opens new window)
    • 代码随想录 (opens new window)
  • 博客系列

    • 美团大佬 (opens new window)
    • 蜡笔小伟 (opens new window)
    • 优秀博客1 (opens new window)
    • 优秀博客2-umi (opens new window)
    • 优质博客 (opens new window)
  • CSS

    • CSS-EASING 库 (opens new window)
    • ROUGH.JS (opens new window)
    • CSS 网站收集
    • UNOCSS (opens new window)
  • 前端

    • PROMISE (opens new window)
    • UNDERSCORE.JS (opens new window)
    • study with BGM (opens new window)
    • nginx【B站视频】 (opens new window)
    • 机器学习
    • Js基础
  • 掘金已购课程

    • 前端自动化测试精讲 (opens new window)
    • 深入浅出 Vite (opens new window)
    • 现代 Web 布局 (opens new window)
    • 前端算法与数据结构 (opens new window)
    • 基于 Vite 的 SSG 框架开发实战 (opens new window)
    • SSR 实战:官网开发指南 (opens new window)
    • WebGL 入门与实践 (opens new window)
    • 玩转 CSS 的艺术之美 (opens new window)
    • 前端调试通关秘籍 (opens new window)
    • React 进阶实践指南 (opens new window)
    • TypeScript 全面进阶指南 (opens new window)
    • 前端缓存技术与方案解析 (opens new window)
    • npm scripts 前端工作流 (opens new window)
    • Webpack5 核心原理与应用实践 (opens new window)
  • 购物车

    • 张鑫旭-技术写作指南 (opens new window)
    • 深入剖析 Node.js 底层原理 (opens new window)
    • 前端开发者的现代 C++ 课 (opens new window)
    • 从前端到全栈 (opens new window)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 编码专题

    • 00.React-目录
    • 10.React-组件-组件封装
    • 11.React-hooks-自定义钩子
      • 0.前言
        • 问题 1:useMemo 如何避免大规模计算
        • 问题 2:改进 useRef
        • 问题 3:如何缓存函数的引用地址?
        • 问题 4:如何处理 useLayoutEffect 在 SSR 环境?
        • 问题 4:useUpdateEffect
        • 问题 5:useMount \ useUnMount
        • 问题 6:useUpdate
        • 问题 7:useCreation = useMemo + useRef
        • 问题 8:useMergeState 封装
        • 问题 9:跨层级通信(事件发布订阅 hooks) + 单例模式
        • 问题 10:如何处理函数的返回值为卸载函数
        • 问题 11: useComposeRef 的写法
        • 问题 12:useAsyncEffect
        • 问题13:usePrevious
      • 参考资料
    • 21.函数技巧 - 函数式编程之 compose 函数
    • 22.函数技巧-tapable钩子
    • 23.函数技巧-koa源码
    • 30.Promise系列-目录
    • 31.Promise-深入Promise原理
    • 32.Promise-指令式Promise改造
    • 32.promise-梳理 Promise 错误处理
    • 34.promise-竞态问题
    • 35.Promise-如何处理异步数组
  • 深入浅出 Vite

  • 快速上手 API

  • 深入浅出Babel

  • 深入浅出 React

  • 百问掘金
  • 编码专题
wangjiasheng
2023-09-10
目录

11.React-hooks-自定义钩子

# 0.前言

代码仓库地址:

# 问题 1:useMemo 如何避免大规模计算

此函数为 useMemo 和 useRef 的合并版。

举例:使用 usePow([1,2,3]),只会产生一次计算。

const usePow = (list: number[]) => {
  return useMemo(
    () =>
      list.map((item: number) => {
        return Math.pow(item, 2);
      }),
    [],
  );
};
1
2
3
4
5
6
7
8
9

# 问题 2:改进 useRef

useRef 的通常的作用:

  1. 解决闭包问题。
  2. 缓存引用地址。

存在的问题:

  1. 没有初始化状态

    // useState 支持接受一个函数
    const [value, setValue] = useState(() => {});
    
    // 错误写法 useRef 不支持
    const valueRef = useRef(() => {
      return 0;
    });
    
    1
    2
    3
    4
    5
    6
    7
  2. 实例化过程会多次执行

    class Foo {
      constructor() {
        this.data = Math.random();
      }
      data: number;
    }
    
    // new Foo 每次均会执行
    const valueRef = useRef(new Foo());
    
    // 如果能实现:
    const valueRef = useRef(() => new Foo());
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

解决方案

如果要解决初始化问题,很简单,封装下面这个钩子即可:

const useRef = <T>(defaultValue: T | (() => T)) => {
  const objRef = useRef();
  // 首次加载
  useFirstMount(() => {
    objRef.current =
      typeof defaultValue === "function" ? defaultValue() : defaultValue;
  });

  return objRef;
};
1
2
3
4
5
6
7
8
9
10

上述改造后,无法实现 useRef 缓存引用地址的功能,如下:

const useLatest = <T>(value: T) => {
  const ref = useRef(value);
  ref.current = value;
  return ref;
};
1
2
3
4
5

因此,最终的解决方案,就是设计一个新的 hooks :useCreation。该自定义钩子会在后续章节单独介绍

const valueRef = useCreation(() => new Foo(), []);
1

完整示例:

export function useCreation<T>(callback: () => T, deps: any[]) {
  // 缓存 函数返回值;
  const objRef = useRef<T>(null);
  // 是否首次加载
  const firstMountRef = React.useRef(true);
  // 初始化 deps
  const depsRef = useRef(deps);

  /* 首次加载 */
  if (firstMountRef.current) {
    // 初始化结束后
    firstMountRef.current = false;
    // 由于 useRef(()=> callback()) 不支持函数初始化赋值。
    objRef.current = callback();
  }

  /* 更新阶段 */
  if (!firstMountRef.current && !depsAreSame(depsRef.current, deps)) {
    // 更新 deps 依赖
    depsRef.current = deps;
    objRef.current = callback();
  }

  return objRef;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

注:这边返回值稍有不同,这里为 objRef ,而非 objRef.current。

# 问题 3:如何缓存函数的引用地址?

正常情况下,我们缓存一个函数的引用地址非常麻烦。

特别是处理 useCallback 的依赖数组。

import { useEvent } from "./useEvent";
import React, { useCallback, useState } from "react";

const Demo: React.FC<any> = React.memo((props) => {
  /* 环境变量 */
  const [count, setCount] = useState(0);

  // ❎ 错误案例:存在闭包问题
  const onClick = useCallback(() => {
    alert(count);
  }, []); /* 缺少 count 变量*/

  const onClick2 = useCallback(() => {
    alert(count);
  }, [count]);

  const onClick3 = useEvent(() => {
    alert(count);
  });

  return (
    <>
      <button onClick={() => setCount(count + 1)}>count+1</button>
      <p>{count}</p>
      <button onClick={onClick}>闭包现象</button>
      <button onClick={onClick2}>useCallback 依赖数组</button>
      <button onClick={onClick3}>useEvent</button>
    </>
  );
});

export default Demo;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

点击 <button> 闭包现象,函数体内的环境变量始终停止在 0 (定义时的变量),即产生闭包现象。

现在希望封装一个钩子,省略掉依赖数组,类似于如下钩子:

  1. rc-util 中的 useEvent
  2. ahooks 中的 useMemoizedFn

特别是在自定义 useXXX 钩子中,一定特别注意对入参为 fn 函数,且当此函数定义时使用到了外部变量时极其容易产生闭包,因此在函数设计时一定要使用 useRef 缓存。

// onChange 函数
const [envVar, setEnvVar] = useState();
const onChange = () => {
  console.log(envVar);
};

function useXXX(params) {
  // 注:当 onChange 函数定义时,使用到了外部变量 envVar
  const { onChange } = params;
  const onChangeFn = useEvent(onChange);
  const onChnageFn = useMemoizedFn(onChange);

  useEffect(() => {
    onChange("xxxx");
    return () => {
      onChange("yyyy");
    };
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

useEvent 、useMemoizedFn 的源码部分虽然不难,但是设计的非常巧妙。

对于函数来说,可以通过下面这种方式无线层的包裹。

const fn1 = (arg1, arg2) => {};
// 对入参 arg1, arg2 继承
export const fn2 = (arg1, arg2) => {
  fn1(arg1, arg2);
};
1
2
3
4
5

在 js 编程中不会这么写,但是对于 hooks 编程来说,正好可以利用到这多包裹一层的箭头函数。

为避免重复渲染,可以使用 useCallback 缓存函数对包裹的箭头函数进行缓存,这样既解决了 deps 的问题,也可以锁死引用地址。

var envVar; /* 环境变量 */

// fn1 引用地址发生变化
const fn1 = (arg1, arg2) => {
  /* 使用到了 envVar */
};

// fn2 固定引用地址
export const fn2 = React.useCallback((arg1, arg2) => fn1(arg1, arg2), []);
1
2
3
4
5
6
7
8
9

useEvent 完整写法:

function useEvent(callback) {
  // 始终缓存最新值,以下也可简写为 useLatest(callback);
  const fnRef = useRef<any>();
  fnRef.current = callback;

  const memoFn = React.useCallback<T>(
    (...args: any) => fnRef.current?.(...args) as any,
    [],
  );

  return memoFn;
}
1
2
3
4
5
6
7
8
9
10
11
12

# 问题 4:如何处理 useLayoutEffect 在 SSR 环境?

在 SSR 环境下,不支持 useLayoutEffect 自定义钩子,需进行降级 useLayoutEffect → useEffect。

在 SSR 无法使用 DOM,因此可以封装 canUseDom 来判断当前环境:

function canUseDom() {
  return !!(
    typeof window !== "undefined" &&
    window.document &&
    window.document.createElement
  );
}
1
2
3
4
5
6
7

这里 !!() 将 window.document.createElement 进行转化。

const useLayoutEffect =
  process.env.NODE_ENV !== "test" && canUsedom()
    ? React.useLayoutEffect
    : React.useEffect;
1
2
3
4

# 问题 4:useUpdateEffect

注:此钩子函数使用到的 useLayoutEffect 最好都做下降级兼容性处理。

首次更新阶段自定义 hooks:

  • 同步钩子:useLayoutUpdateEffect
  • 异步钩子:useUpdateEffect

最简单的封装方式:

function useLayoutUpdateEffect = (callback,deps){
  const firstMountRef = React.useRef(true);
  // 将 useLayoutEffect 换成 useEffect 即可变为 useUpdateEffect
  useLayoutEffect(()=>{
    if(firstMountRef.current){
      // mount 阶段
      firstMountRef.current = false;
    }else {
      // update 阶段
      callback();
    };
  }, deps);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

更进一步考虑到,可以返回 firstMountRef 判断当前组件是否已经卸载。

function useLayoutUpdateEffect = (callback,deps){
  // 是否首次更新
  const firstMountRef = React.useRef(true);

  // 将 useLayoutEffect 换成 useEffect 即可变为 useUpdateEffect
  useLayoutEffect(()=>{
    if(!firstMountRef.current){
      return callback();
    }
  }, deps);

  // 支持 unMount 处理
  useLayoutEffect(()=>{
    firstMountRef.current = false;
    return ()=>{
      firstMountRef.current = true;
    }
  },[]);

  return firstMountRef;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

测试案例:

import { useBoolean, useUnmountedRef } from "ahooks";
import { message } from "antd";
import React, { useEffect } from "react";

const MyComponent = () => {
  const unmountedRef = useUnmountedRef();
  useEffect(() => {
    setTimeout(() => {
      if (!unmountedRef.current) {
        message.info("component is alive");
      }
    }, 3000);
  }, []);

  return <p>Hello World!</p>;
};

export default () => {
  const [state, { toggle }] = useBoolean(true);

  return (
    <>
      <button type="button" onClick={toggle}>
        {state ? "unmount" : "mount"}
      </button>
      {state && <MyComponent />}
    </>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 问题 5:useMount \ useUnMount

  • 首次加载触发钩子:
const useMount = (callback: () = >void ) => {
  useEffect(()=>{
    // mount 阶段
    callback?.();
  },[]);
}
1
2
3
4
5
6
  • 卸载时触发

这个需要注意一个点,就是使用 useRef 来确保所传入的函数为最新的状态

const useUnmount = (callback: () => void) => {
  // 初始化执行
  const funRef = useRef();

  // 每次函数执行
  funRef.current = callback;

  // 这里涉及闭包
  useEffect(
    () => () => {
      funRef.current?.();
    },
    [],
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

以上实现基于 useEffect 实现,同样可以实现 useLayoutEffect 版本

const useLayoutEffect =
  process.env.NODE_ENV !== "test" && canUsedom()
    ? React.useLayoutEffect
    : React.useEffect;

const useLayoutMount = (callback: () => void) => {
  useLayoutEffect(() => {
    // mount 阶段
    callback?.();
  }, []);
};

const useLayoutUnmount = (callback: () => void) => {
  const funRef = useRef(callback);
  funRef.current = callback;
  useLayoutEffect(
    () => () => {
      funRef.current?.();
    },
    [],
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

额外说明的是可以使用 useRef 封装一个初始化钩子:

const useFirstMount = (callback: () => void) => {
  const isFirstMount = useRef(true);
  if (isFirstMount.current) {
    callback();
    isFirstMount.current = false;
  }
};
1
2
3
4
5
6
7

综合测试案例

function TestComp() {
  useFirstMount(() => {
    console.log("useFirstMount-首次加载");
  });
  useMount(() => {
    console.log("useMount-首次加载");
  });
  useLayoutMount(() => {
    console.log("useLayoutMount-首次加载");
  });
  useUnmount(() => {
    console.log("useUnMount-组件卸载");
  });
  useLayoutUnmount(() => {
    console.log("useLayoutUnmount-组件卸载");
  });
  return "Hello world";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 问题 6:useUpdate

如何强制刷新页面

const [, update] = useState({});

update({});
1
2
3

封装成钩子:

function useUpdate() {
  const [, update] = useState({});
  return useCallback(() => update({}), []);
}
1
2
3
4

# 问题 7:useCreation = useMemo + useRef

此钩子使用方式基本等同于 useMemo ,使用场景有限。

该钩子可用于学习,如 从零实现一个 useMemo 官方自定义钩子。

在 React 官网文档中说,被 memo 的值一定不会被重计算。

const res = useMemo(() => {
  return xxx;
}, [deps]);

const res = useCreation(() => {
  return xxx;
}, [deps]);
1
2
3
4
5
6
7

版本 1: ahooks 写法

// 通过 Object.is 比较依赖数组的值是否相等
function depsAreSame(oldDeps: DependencyList, deps: DependencyList): boolean {
  if (oldDeps === deps) return true;
  for (let i = 0; i < oldDeps.length; i++) {
    if (!Object.is(oldDeps[i], deps[i])) return false;
  }
  return true;
}

function useCreation<T>(factory: () => T, deps: DependencyList) {
  const { current } = useRef({
    deps,
    obj: undefined as undefined | T,
    initialized: false,
  });
  // 初始化或依赖变更时,重新初始化
  if (current.initialized === false || !depsAreSame(current.deps, deps)) {
    current.deps = deps; // 更新依赖
    current.obj = factory(); // 执行创建所需对象的函数
    current.initialized = true; // 初始化标识为 true
  }
  return current.obj as T;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

版本 2: 自己的写法,可以把状态变化更清晰分离出来

  1. 完全使用 useRef 实现,需手动实现 deps 对比。
export function useCreation<T>(callback: () => T, deps: any[]) {
  // 缓存 函数返回值;
  const objRef = useRef<T>(null);
  // 是否首次加载
  const firstMountRef = React.useRef(true);
  // 初始化 deps
  const depsRef = useRef(deps);

  /* 首次加载 */
  if (firstMountRef.current) {
    // 初始化结束后
    firstMountRef.current = false;
    // 由于 useRef(()=> callback()) 不支持函数初始化赋值。
    objRef.current = callback();
  }

  /* 更新阶段 */
  if (!firstMountRef.current && !depsAreSame(depsRef.current, deps)) {
    // 更新 deps 依赖
    depsRef.current = deps;
    objRef.current = callback();
  }

  return objRef.current;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

版本 3: 封装 useUpdateEffect 和 useFirstUpdate简化更新阶段写法。

🌟 这里特别注意,返回值为 objRef.current ,因此函数中只能使用同步 hooks 操作。

首次加载只可以使用:useFirstMount ,切忌不要使用 useMount(async) 、useLayoutMount(sync)

首次更新只可以使用:useFisrtUpdate ,切忌不要使用 useUpdateEffec(async)、 useUpdateLayoutEffect(sync)

const useFisrtUpdate = (callback: any) => {
  const isFirstMount = useRef(true);
  if (isFirstMount.current) {
    isFirstMount.current = false;
  } else {
    callback();
  }
};

// ❎ 错误使用
const useLayoutMount = (callback: any) => {
  const isFirstMount = useRef(true);
  // useEffect(async) useLayoutEffect(async)
  useLayoutEffect(() => {
    if (isFirstMount.current) {
      isFirstMount.current = false;
    } else {
      callback();
    }
  });
};

export function useCreation<T>(callback: () => T, deps: any[]) {
  // 缓存 函数返回值;
  const objRef = useRef<T | undefined>();

  // 初始化 deps
  const depsRef = useRef(deps);

  /* 首次加载 */
  useFirstMount(() => {
    objRef.current = callback();
  });

  /* 更新阶段 */
  useFisrtUpdate(() => {
    if (!depsAreSame(depsRef.current, deps)) {
      // 更新 deps 依赖
      depsRef.current = deps;
      objRef.current = callback();
    }
  });
  return objRef.current;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

版本 4: 直接使用 useEffect 或者 useLayoutEffect自带的 deps 监听,简单封装即可。

export function useCreation<T>(callback: () => T, deps: any[]) {
  // 缓存 函数返回值;
  const objRef = useRef<T | undefined>();

  // 标志位
  const [, refresh] = useState({});

  useEffect(() => {
    objRef.current = callback();
    refresh({});
  }, deps);

  return objRef.current;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 问题 8:useMergeState 封装

rc-util 库 useMergedState 函数的使用,基础使用不再赘述。

API 设计:

function useMergedState<T, R = T>(
  defaultStateValue: T | (() => T),
  option?: {
    defaultValue?: T | (() => T);
    value?: T;
    onChange?: (value: T, prevValue: T) => void;
    postState?: (value: T) => T;
  },
): [R, Updater<T>] {
  // 省略逻辑代码......
}
1
2
3
4
5
6
7
8
9
10
11

# 问题 9:跨层级通信(事件发布订阅 hooks) + 单例模式

涉及 hooks: useEventEmitter

首先:实现一个 EventEmitter 类,用于存储 subscriptions 数组, ahooks 考虑的比较简单,省略了 eventName 这个字段,因此整体 hooks 功能比较轻量。

import { useEffect, useRef } from "react";

type Subscription<T> = (val: T) => void;

export class EventEmitter<T> {
  // 采用Set存储订阅回调
  private subscriptions = new Set<Subscription<T>>();

  // 遍历回调函数
  emit = (val: T) => {
    for (const subscription of this.subscriptions) {
      subscription(val);
    }
  };

  /* 自定义 Hooks 可以定义在 Class 结构上 */
  useSubscription = (callback: Subscription<T>) => {
    const callbackRef = useRef<Subscription<T>>();
    callbackRef.current = callback;
    useEffect(() => {
      // 订阅
      function subscription(val: T) {
        if (callbackRef.current) {
          callbackRef.current(val);
        }
      }
      /* 类似:触发 on 事件 */
      this.subscriptions.add(subscription);
      return () => {
        /* 类似:触发 emit 事件 */
        this.subscriptions.delete(subscription);
      };
    }, []);
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

保证实例始终是单例:

/* 单例模式写法 */
export default function useEventEmitter<T = void>() {
  const ref = useRef<EventEmitter<T>>();
  if (!ref.current) {
    ref.current = new EventEmitter();
  }
  return ref.current;
}
1
2
3
4
5
6
7
8

使用方案:

使用 Context 或 props 顶部缓存。

import { useRef, FC } from "react";
import useEventEmitter from "./useEventEmitter";
import { EventEmitter } from "./useEventEmitter";

const MessageBox: FC<{
  focus: EventEmitter<void>;
}> = function(props) {
  const { focus } = props;
  return (
    <button onClick={() => /* emit 触发事件 */ focus.emit()}> emit </button>
  );
};

const InputBox: FC<{
  focus: EventEmitter<void>;
}> = function(props) {
  const inputRef = useRef<any>();
  const { focus } = props;

  /* on 事件 */
  focus.useSubscription(() => {
    inputRef.current.focus();
  });
  return <input ref={inputRef} />;
};

export default function() {
  /* 保证单例 */
  const focus = useEventEmitter();
  return (
    <>
      <MessageBox focus={focus} />
      <InputBox focus={focus} />
    </>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

此 hooks 过于简单,若希望支持 eventName ,可参考这篇文章实现 《React 中优雅的使用 useEventEmitter 进行多组件通信》 (opens new window)

# 问题 10:如何处理函数的返回值为卸载函数

# 问题 11: useComposeRef 的写法

当存在多个 ref 绑定到 dom 结构时,可以使用 ref 透传函数

import React, { useRef } from "react";
const Demo: React.FC<any> = React.memo((props) => {
  const formRef1 = useRef<any>();
  const formRef2 = useRef<any>();
  return (
    <>
      <div
        ref={(node) => {
          formRef1.current = node;
          formRef2.current = node;
        }}
      >
        Hello World
      </div>
      <button
        onClick={() => {
          console.log("formRef1", formRef1.current);
          console.log("formRef2", formRef2.current);
        }}
      >
        获取 ref 对象
      </button>
    </>
  );
});
export default Demo;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

此时,也可以使用 rc-util 暴露出的 useComposeRef 自定义钩子或 composeRef 函数。

使用方式如下:

const formRef1 = useRef<any>();
const formRef2 = useRef<any>();

// 以下两种使用方式等价:
const mergedRef = useComposeRef(formRef1, formRef2);
const mergedRef2 = useMemo(()=>{
   return composeRef(formRef1,formRef2);
}, [formRef1,formRef2])

// 对应的 DOM 结构绑定
<div ref={mergedRef}></div>
1
2
3
4
5
6
7
8
9
10
11

完整示例

import React, { useRef } from "react";

import { useComposeRef } from "./useComposeRef";

const Demo: React.FC<any> = React.memo((props) => {
  const formRef1 = useRef<any>();
  const formRef2 = useRef<any>();

  // useComposeRef 或者 composeRef 额外支持一种函数写法,(node)=>{}
  const mergedRef = useComposeRef(formRef1, (node) => {
    console.log("node", node);
    formRef2.current = node;
  });

  return (
    <>
      <div ref={mergedRef}>Hello World</div>
      <button
        onClick={() => {
          console.log("formRef1", formRef1.current);
          console.log("formRef2", formRef2.current);
        }}
      >
        获取 ref 对象
      </button>
    </>
  );
});

export default Demo;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

源码实现:

export function composeRef<T>(...refs: React.Ref<T>[]): React.Ref<T> {
  // 过滤非空
  const refList = refs.filter(Boolean);

  // 处理只有1个情况
  if (refList.length <= 1) {
    return refList[0];
  }

  return (node: T) => {
    refs.forEach((ref) => {
      // ref 支持接受如:(node)=>{ domRef.current = node} 形式写法
      if (typeof ref === "function") {
        ref(node);
      } else if (typeof ref === "object" && ref && "current" in ref) {
        (ref as any).current = node;
      }
    });
  };
}

// 对 composeRef 做了一个 memo 处理
export function useComposeRef<T>(...refs: React.Ref<T>[]): React.Ref<T> {
  return useMemo(() => composeRef(...refs), refs);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 问题 12:useAsyncEffect

# 问题13:usePrevious

通过 useEffect 只能监听到某一个值的变化,而无法监听值变化的方向。

const App = ({initialState})=>{
  const {isLogin} = initialState;
  useEffect(()=>{
    // 只希望 isLogin: false → true 触发某个操作
  },[isLogin])
}
1
2
3
4
5
6

可以直接使用 ref 对需要监听的变量进行操作:

const App = ({initialState})=>{
  const {isLogin} = initialState;
  const preRef = useRef(isLogin);
  const curRef = useRef(isLogin);
  // 更新状态
  preRef.current = curRef.current;
  curRef.current = isLogin;
  
  if(preRef.currrent === false &&
     curRef.current === true 
   ){
    // 触发更新操作
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

进一步可以将上述过程封装成钩子 usePrevious

import { useRef } from 'react';

export type ShouldUpdateFunc<T> = (prev: T | undefined, next: T) => boolean;

const defaultShouldUpdate = <T>(a?: T, b?: T) => !Object.is(a, b);

function usePrevious<T>(
  state: T,
  shouldUpdate: ShouldUpdateFunc<T> = defaultShouldUpdate,
): T | undefined {
  const prevRef = useRef<T>();
  const curRef = useRef<T>();

  // 嵌入更新逻辑控制,是否修改 preRef.current 
  if (shouldUpdate(curRef.current, state)) {
    prevRef.current = curRef.current;
    curRef.current = state;
  }

  return prevRef.current; // shouldUpdate 返回 true 为 preRef 为最新值
}

export default usePrevious;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

使用时:

const App = ({initialState})=>{
  const {isLogin} = initialState;
  const _isLogin = usePrevious(isLogin,(preState,curState)=>{
    if(preState === false && curState === true){
      // 触发更新操作
    }
    return true;
  })
}
1
2
3
4
5
6
7
8
9

# 参考资料

  1. 《解读 ahooks 源码系列】Advanced 篇》 (opens new window)

  2. 《ahooks 源码系列(六):功能性相关的 hook》 (opens new window)

  3. 《React Hook 大师之路:ahooks 常用 Hook 源码学习》 (opens new window)

  4. 《如何让 useEffect 支持 async...await?》 (opens new window)

  5. 《搞懂这 12 个 Hooks,保证让你玩转 React》 (opens new window)

编辑 (opens new window)
上次更新: 2023/11/05, 21:11:00
10.React-组件-组件封装
21.函数技巧 - 函数式编程之 compose 函数

← 10.React-组件-组件封装 21.函数技巧 - 函数式编程之 compose 函数→

最近更新
01
如何理解浏览器的 user agent 用户代理的含义?
11-05
02
浏览器事件循环机制
10-31
03
浏览器页面渲染机制【2023】
10-15
更多文章>
Theme by Vdoing | Copyright © 2020-2023
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式