关于前端:不会React-hooks怎么办试一试读Ahooks源码吧~

9次阅读

共计 8530 个字符,预计需要花费 22 分钟才能阅读完成。

背景

  • 迁徙我的项目中,随处可见 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 = c , 否则a = b
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 关键字能够对传入的进来的范型类型进行限度。

举个简略的🌰

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 中函数的入参类型和返回类型。

3.2 useCreation

useCreation 是 useMemo 或 useRef 的替代品。

举个简略的🌰

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 能够独自拿进去讲讲。期待下一篇吧。

正文完
 0