不会React hooks怎么办 ,Ahooks前来助力!!(源码解析系列)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 不会React hooks怎么办 ,Ahooks前来助力!!(源码解析系列)

背景

  • 迁移项目中,随处可见ahooks的身影,在抱有好奇心以及探索 React 中 自定义 hook 的最佳实践的过程中。于是便有这篇了ahooks源码解析系列。
  • ahooks中有大量的TS定义,可以从中吸取到很多的代码设计,快速上手React+TS开发模式。
  • 简单快速即可上手阅读ahooks源码,低耦合性也让代码结构更加清晰,调试者也不需要关注复杂的逻辑。

官方文档

一) 介绍

ahooks,发音 [eɪ hʊks],是一套高质量可靠的 React Hooks 库。在当前 React 项目研发过程中,一套好用的 React Hooks 库是必不可少的,希望 ahooks 能成为您的选择。

特性

  • 易学易用
  • 支持 SSR
  • 对输入输出函数做了特殊处理,且避免闭包问题
  • 包含大量提炼自业务的高级 Hooks
  • 包含丰富的基础 Hooks
  • 使用 TypeScript 构建,提供完整的类型定义文件

安装

$ npm install --save ahooks
# or
$ yarn add ahooks
# or
$ pnpm add ahooks

使用

import { useRequest } from 'ahooks';

二)拉取ahooks代码

将仓库 ahooks clone 到本地

拉起下来的代码里面会有很多工程化的文件,这里就不会做过多介绍了,因为是即便完全不懂这些东西,也不妨碍你可以轻松的调试ahooks源码。

CONTRIBUTING.zh-CN文件中有其贡献指南和启动项目的流程。

pnpm install 
pnpm run init

代码运行起来之后就可以在本地看到一份和官网一模一样的文档了。

三)常用Hook源码解析

目前部门主要采用的是 Mobx+React+TS 以及自研组件库,关于操作视图层的hooks这里就不做过多介绍了,感兴趣的可以自己研究一下~

3.1 useDebounceFn

用来处理防抖函数的 Hook。用法和 debounce 非常类似。

const [value, setValue] = useState(0);
  const { run } = useDebounceFn(
    () => {
      setValue(value + 1);
    },
    {
      wait: 500,
    },
  );

补充一点:

  • 空值合并运算符 ?? ,a = b ?? c 只要 b 不为nullor undefineda = b ,否则a = c
  • 空值合并操作符?? )是一个逻辑操作符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。
let a = 1
let b = 2
const c = a ?? b // c = 1
b = undefined
const d = b ?? a // d = 1
核心代码
import debounce from 'lodash/debounce';
import { useMemo } from 'react';
import type { DebounceOptions } from '../useDebounce/debounceOptions';
import useLatest from '../useLatest';
import useUnmount from '../useUnmount';
import { isFunction } from '../utils';
type noop = (...args: any) => any;
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
  ... 省去部分代码
  // 永远使用最新的fn
  const fnRef = useLatest(fn);
  // 空值校验
  const wait = options?.wait ?? 1000;
  // 思考一下 这里为什么要使用useMemo来包一层呢 ?
  // 其实hook也是一个函数,当组件reRender的时候hook也会重新执行一变,所以需要useMemo来记录已经保存下来的结果
  const debounced = useMemo(
    () =>
      debounce(
        (...args: Parameters<T>): ReturnType<T> => {
          return fnRef.current(...args);
        },
        wait,
        options,
      ),
    [],
  );
  // 组件销毁时,取消防抖函数调用。防止造成内存泄漏
  useUnmount(() => {
    debounced.cancel();
  });
  return {
    run: debounced,
    cancel: debounced.cancel,
    flush: debounced.flush,
  };
}
export default useDebounceFn;

useLastest.ts

这个 hook 的使用场景目前还没找到很好的答案。如果按照这个 实现,每次获取最新的值,那为什么不直接使用 value 呢?

在和一位大佬探讨后,目前得到的结果就是为了适应某些闭包场景。

import { useRef } from 'react';
function useLatest(value) {
  var ref = useRef(value);
  ref.current = value;
  return ref;
}
export default useLatest;
从useDebounceFn可以学习到的TS编码
type noop = (...args: any) => any;
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) 

在TS中extends关键字可以对传入的进来的范型类型进行限制。

举个简单的🌰

image.png

function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
  ... 
  const debounced = useMemo(
    () =>
      debounce(
        (...args: Parameters<T>): ReturnType<T> => {
          return fnRef.current(...args);
        },
        wait,
        options,
      ),
    [],
  );
  .... 
}
  • ParametersReturnType 可以分别获取TS中函数的入参类型和返回类型。

image.png

image.png

3.2 useCreation

useCreation是 useMemouseRef 的替代品。

举个简单的🌰

sandbox

这里每次修改count的值,getRrandomNum都会被重新执行(执行两次是因为React中的严格模式...)

换成useCreaction就完美解决了这个问题 sandbox

核心代码
import type { DependencyList } from 'react';
import { useRef } from 'react';
import depsAreSame from '../utils/depsAreSame';
export default function useCreation<T>(factory: () => T, deps: DependencyList) {
  const { current } = useRef({
    deps,
    obj: undefined as undefined | T,
    initialized: false,
  });
  / *
    * 虽然useCreation函数会随着组件的reRender而重新执行
    * 但是factory函数只有首次进来或者deps依赖发生改变才会重新执行
    */
  if (current.initialized === false || !depsAreSame(current.deps, deps)) {
    current.deps = deps;
    current.obj = factory();
    current.initialized = true;
  }
  return current.obj as T;
}

depsAreSame

功能:对比两个依赖是否相等

import type { DependencyList } from 'react';
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;
}
从useCreation中学到的TS技巧
  • import type ... from让编译器知道要导入的内容绝对是一种类型。详情见 你不知道的import type)
  • undefined as undefined | T当给一个变量值,并且需要限制类型的时候,可以通过 as 类型断言操作。

3.3 useSize

监听 DOM 节点尺寸变化的 Hook。

使用场景: 当某个元素的大小发生改变时,需要进行一系列操作。

举个🌰: 当我们使用 echarts 绘制图标的时候就会出现这样的问题。当 echarts 的容器的大小是自适应单位,如rem vw等。我们希望绘制出来的图标也可以跟随容器大小改变而改变。

const size = useSize(EchartsDomRef);
  useEffect(() => {
    if (chartRef.current) {
      chartRef.current.resize(); // 当容器宽度发生改变的时候,调用resize方法重新渲染echarts
    }
  }, [size.width]);
  useEffect(() => {
    chartRef.current = echarts.init(EchartsDomRef.current);
  }, []);
核心代码
import ResizeObserver from 'resize-observer-polyfill';
import useRafState from '../useRafState';
import type { BasicTarget } from '../utils/domTarget';
import { getTargetElement } from '../utils/domTarget';
import useIsomorphicLayoutEffectWithTarget from '../utils/useIsomorphicLayoutEffectWithTarget';
type Size = { width: number; height: number };
function useSize(target: BasicTarget): Size | undefined {
  const [state, setState] = useRafState<Size>();
  useIsomorphicLayoutEffectWithTarget(
    () => {
      const el = getTargetElement(target);
      if (!el) {
        return;
      }
      const resizeObserver = new ResizeObserver((entries) => {
        entries.forEach((entry) => {
          const { clientWidth, clientHeight } = entry.target;
          setState({
            width: clientWidth,
            height: clientHeight,
          });
        });
      });
      resizeObserver.observe(el);
      return () => {
        resizeObserver.disconnect();
      };
    },
    [],
    target,
  );
  return state;
}
export default useSize;

3.3 useUnmountedRef

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

使用场景: 发送网络请求前/后,判断组件是否已经销毁。如果销毁取消本次请求/减少后续的一系列操作。

举个🌰

const unMounted = useUnmountedRef();
getData.then(res => {
    if (unMounted.current) {
      return;
    }
    ....一些列耗时的操作
})
核心代码

这个hook的实现很简单。在组件挂载在dom上的时候设置值false,当组件销毁的时候设置为true

import { useEffect, useRef } from 'react';
const useUnmountedRef = () => {
  const unmountedRef = useRef(false);
  useEffect(() => {
    unmountedRef.current = false;
    return () => {
      unmountedRef.current = true;
    };
  }, []);
  return unmountedRef;
};
export default useUnmountedRef;

3.4 useMemoizedFn

持久化 function 的 Hook,理论上,可以使用 useMemoizedFn 完全代替 useCallback。

核心代码

使用过 Vue3 的一定不会陌生WatchEffect的自动收集依赖机制;

这里实现也很巧妙,re-render 阶段不断更新 fn.Ref.current 的引用值。 但是memoizedFn.current指向的函数返回值就是函数 fn 最新的返回值,同时memoizedFn只会在挂载阶段赋值一次,这样就确保了memoizedFn.current 的引用地址保持不变。

import { useMemo, useRef } from 'react';
type noop = (this: any, ...args: any[]) => any;
type PickFunction<T extends noop> = (
  this: ThisParameterType<T>,
  ...args: Parameters<T>
) => ReturnType<T>;
function useMemoizedFn<T extends noop>(fn: T) {
  const fnRef = useRef<T>(fn);
  // why not write fnRef.current = fn?
  // https://github.com/alibaba/hooks/issues/728
  fnRef.current = useMemo(() => fn, [fn]);
  const memoizedFn = useRef<PickFunction<T>>();
  if (!memoizedFn.current) {
    memoizedFn.current = function (this, ...args) {
      return fnRef.current.apply(this, args);
    };
  }
  return memoizedFn.current as T;
}

3.5 useLockFn

用于给一个异步函数增加 竞 态锁,防止并发执行。

使用场景: 在许多场景中都可以使用useLockFn来减少网络上的开销。

举个简单的🌰 如果现在有个上拉加载的函数loadMore。这个函数需要进行网络请求才会返回最终的结果。一般的做法就是通过设置isLoading状态来判断函数是否执行。但是有了useLockFn我们的代码就会变得简易许多,业务逻辑也会变得更加清晰。

const loadMore = async () => {
    if(isLoading) return 
    isLoading = true
    const data = await getMockData(...parasms)
    isLoading = false
} 
核心代码

实现也非常简单,就是利用了useRef在整个生命周期只会初始化一次,来记录一个状态变量,判断 fn 是否执行完毕。

import { useRef, useCallback } from 'react';
function useLockFn<P extends any[] = any[], V extends any = any>(fn: (...args: P) => Promise<V>) {
  const lockRef = useRef(false);
  return useCallback(
    async (...args: P) => {
      if (lockRef.current) return;
      lockRef.current = true;
      try {
        const ret = await fn(...args);
        lockRef.current = false;
        return ret;
      } catch (e) {
        lockRef.current = false;
        throw e;
      }
    },
    [fn],
  );
}

但这里有个小问题 ?大家知道为什么这里需要使用useCallback来包一层嘛,而不是直接返已经处理了 竞态🔒 逻辑的函数呢?

答案也很简单,当传入函数fn并不是一个临时函数,而是一个引用。当fn的引用地址未发生改变的时候,就防止了useLockFn函数返回结果发生改变,有可能会造成子组件的re-render。

四)Other hook

4.1 useUpdateEffect

useUpdateEffect用法等同于 useEffect,但是会忽略首次执行,只在依赖更新时执行。

import { useRef } from 'react';
import type { useEffect, useLayoutEffect } from 'react';
type EffectHookType = typeof useEffect | typeof useLayoutEffect;
export const createUpdateEffect: (hook: EffectHookType) => EffectHookType =
  (hook) => (effect, deps) => {
    const isMounted = useRef(false);
    // for react-refresh
    hook(() => {
      return () => {
        isMounted.current = false;
      };
    }, []);
    // update 
    hook(() => {
      if (!isMounted.current) {
        isMounted.current = true;
      } else {
        return effect();
      }
    }, deps);
  };
const useUpdateEffect = createUpdateEffect(useEffect);

首次进入函数会执行两次 hook 这里其实就是useEffect设置状态变量isMountedtrue,接下来每次更新就直接执行effect函数。

4.2 useSetState

管理 object 类型 state 的 Hooks,用法与 class 组件的 this.setState 基本一致。

import { useCallback, useState } from 'react';
import { isFunction } from '../utils';
export type SetState<S extends Record<string, any>> = <K extends keyof S>(
  state: Pick<S, K> | null | ((prevState: Readonly<S>) => Pick<S, K> | S | null),
) => void;
const useSetState = <S extends Record<string, any>>(
  initialState: S | (() => S),
): [S, SetState<S>] => {
  const [state, setState] = useState<S>(initialState);
  // 入参为函数的时候直接执行函数,得到返回值再与旧值扩展合并。
  const setMergeState = useCallback((patch) => {
    setState((prevState) => {
      const newState = isFunction(patch) ? patch(prevState) : patch;
      return newState ? { ...prevState, ...newState } : prevState;
    });
  }, []);
  return [state, setMergeState];
};

4.3 usePrevious

保存上一次状态的 Hook。一般用于缓存状态。

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>();
  // 进行 shallow equal 比较
  if (shouldUpdate(curRef.current, state)) { 
    prevRef.current = curRef.current;
    curRef.current = state;
  }
  return prevRef.current;
}

四)总结

最后!!!学习 ahooks 一定是你 React 新手进阶最好的源码库。

第一篇关于 Ahooks源码解析 就这样结束了,感觉useRequestuseUrlState这两个 hook 可以单独拿出来讲讲。期待下一篇吧。


相关文章
|
4天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
16 2
|
30天前
|
前端开发 JavaScript
React Hooks 全面解析
【10月更文挑战第11天】React Hooks 是 React 16.8 引入的新特性,允许在函数组件中使用状态和其他 React 特性,简化了状态管理和生命周期管理。本文从基础概念入手,详细介绍了 `useState` 和 `useEffect` 的用法,探讨了常见问题和易错点,并提供了代码示例。通过学习本文,你将更好地理解和使用 Hooks,提升开发效率。
65 4
|
1月前
|
前端开发
深入解析React Hooks:构建高效且可维护的前端应用
本文将带你走进React Hooks的世界,探索这一革新特性如何改变我们构建React组件的方式。通过分析Hooks的核心概念、使用方法和最佳实践,文章旨在帮助你充分利用Hooks来提高开发效率,编写更简洁、更可维护的前端代码。我们将通过实际代码示例,深入了解useState、useEffect等常用Hooks的内部工作原理,并探讨如何自定义Hooks以复用逻辑。
|
4天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
17天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
37 3
|
1月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
53 5
|
1月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
67 0
|
1月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
52 0
|
1月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
60 0
|
1月前
|
安全 Java 程序员
Collection-Stack&Queue源码解析
Collection-Stack&Queue源码解析
80 0

推荐镜像

更多