ahooks 源码学习

2024/06/27 react 共 6668 字,约 20 分钟

背景

最近在做项目优化的事情,想学习一下ahooks是如何实现useRequest的,之前有个朋友实现滚动加载hooks的时候,他也提到了参照ahooks的实现。于是我带着好奇来谈一谈,去年开始下半年开始在项目中使用react,到现在不过一年左右。自我感觉,对于react我不过是做项目做需求没任何问题,会使用罢了。正好接这个机会好好学习一下,证实一下hooks的魅力。

hooks 封装UI组件和逻辑

hooks 不止是可以封装逻辑,还能是 ui 也能一起处理,这个是我之前没有想到的。封装了UI 组件就变得非常强大。比如在我项目中的一个antd 弹窗组件的封装

import { Modal, Space, Button, Spin } from "antd";
import type { ModalFuncProps, ModalProps } from "antd";
import { type ReactNode } from "react";
import "@/index.css";

const useConfirmModal = (data?: {
  title?: string;
  content?: ReactNode;
  modalProps?: ModalProps;
}) => {
  const [modal, contextHolder] = Modal.useModal();

  const showModal = (body: ModalFuncProps & { loading?: boolean }) => {
    const modalProps = data?.modalProps;
    const props = {
      width: 520,
      ...modalProps,
      ...body,
      icon: body?.icon ?? null,
      closable: body?.closable ?? true,
      centered: body?.centered ?? true,
    };
    const createContent = () => body.content ?? data?.content;
    const createFooter = ({ loading }: { loading: boolean }) => {
      return (
        <div className='flex justify-end mt-[12px]'>
          <Space>
            {props.cancelText !== null && (
              <Button
                type={props.cancelButtonProps?.type || "default"}
                onClick={async () => {
                  body.onCancel && (await body.onCancel());
                  modalText.destroy();
                }}
                disabled={loading}
              >
                {props.cancelText || "取消"}
              </Button>
            )}
            {props.okText !== null && (
              <Button
                type='primary'
                onClick={async () => {
                  if (body.loading) {
                    modalText.update({
                      closable: false,
                      // maskClosable: false,
                      content: getContent({ loading: true }),
                    });
                    body.onOk &&
                      body
                        .onOk()
                        .then(() => {
                          modalText.destroy();
                        })
                        .catch(() => {
                          modalText.update({
                            ...props,
                            content: getContent({ loading: false }),
                          });
                        });
                  } else {
                    body.onOk && (await body.onOk());
                    modalText.destroy();
                  }
                }}
                loading={loading}
              >
                {props.okText || "确定"}
              </Button>
            )}
          </Space>
        </div>
      );
    };
    const getContent = ({ loading }: { loading: boolean }) => {
      return (
        <>
          {createContent()}
          {createFooter({ loading })}
        </>
      );
    };
    const modalText = modal.confirm({
      ...props,
      content: getContent({ loading: false }),
      footer: null,
    });
  };

  return {
    showModal,
    contextHolder,
  };
};
export default useConfirmModal;

甚至我可以在一个hooks中既有UI又有逻辑,逻辑和UI皆可对外暴露,那这个就很强了

hooks 闭包问题

type Hook = { memoizedState: any; baseState: any; baseUpdate: Update<any, any> | null; queue: UpdateQueue<any, any> | null; next: Hook | null; };

这个对象的 memoizedState 属性就是用来存储组件上一次更新后的 state,next 指向下一个 hook 对象。在组件更新的过程中,hooks 函数执行的顺序是不变的,就可以根据这个链表拿到当前 hooks 对应的 Hook 对象,函数式组件就是这样拥有了 state 的能力。

同时制定了一系列的规则,比如不能将 hooks 写入到 if…else… 中。从而保证能够正确拿到相应 hook 的 state。

介绍一下 useEffect,它接收了两个参数,一个回调函数和一个数组。数组里面就是 useEffect 的依赖,当为 [] 的时候,回调函数只会在组件第一次渲染的时候执行一次。如果有依赖其他项,react 会判断其依赖是否改变,如果改变了就会执行回调函数。

它第一次执行的时候,执行 useState,count 为 0。执行 useEffect,执行其回调中的逻辑,启动定时器 setInterval: 0 ,每隔 1s 输出 0 。

当我点击按钮使 count 增加 1 的时候,整个函数式组件重新渲染,这个时候前一个执行的链表已经存在了。useState 将 Hook 对象上保存的状态置为 1, 那么此时 count 也为 1 了。执行 useEffect,其依赖项为空,不执行回调函数。但是之前的回调函数还是在的,它还是会每隔 1s 执行 console.log(“setInterval:”, count);,但这里的 count 是之前第一次执行时候的 count 值,因为在定时器的回调函数里面被引用了,形成了闭包一直被保存。

ahooks 介绍

源码地址

packages/hooks/package.json。重点关注一下 dependencies 和 peerDependencies peerDependencies 的目的是提示宿主环境去安装满足插件 peerDependencies 所指定依赖的包,然后在插件 import 或者 require 所依赖的包的时候,永远都是引用宿主环境统一安装的 npm 包,最终解决插件与所依赖包不一致的问题。这里的宿主环境一般指的就是我们自己的项目本身了。当你写的包 a 里面依赖另一个包 b,而这个包 b 是引用这个包 a 的业务的常用的包的时候,建议写在 peerDependencies 里,避免重复下载/多个版本共存。

useRequest

入口 useRequest。它负责的是初始化处理数据以及将结果返回。通过插件化机制降低了每个功能之间的耦合度,也降低了其本身的复杂度

Fetch。是整个 useRequest 的核心代码,它处理了整个请求的生命周期。Fetch 类的代码会变得非常的精简,只需要完成整体流程的功能,所有额外的功能(比如重试、轮询等等)都交给插件去实现

plugin。在 Fetch 中,会通过插件化机制在不同的时机触发不同的插件方法,拓展 useRequest 的功能特性。一个 Plugin 只做一件事,相互之间不相关。整体的可维护性更高,并且拥有更好的可测试性

utils 和 types.ts。提供工具方法以及类型定义。

如何使用插件化机制优雅的封装你的请求

useRequest 是 ahooks 最核心的功能之一,它的功能非常丰富,但核心代码(Fetch 类)相对简单,这得益于它的插件化机制,把特定功能交给特定的插件去实现,自己只负责主流程的设计,并暴露相应的执行时机即可。

api 请求成功后重新渲染组件

useRequest 是 ahooks 中的一个 hooks,它是用来处理请求的。我那时候非常好奇,为啥请求成功了,会重新渲染组件。这个是怎么实现的呢?

原来底层用到了useState,他的实现原理也非常的简单,代码如下

const useUpdate = () => {
  const [, setState] = useState({});

  return useCallback(() => setState({}), []);
};

export default useUpdate;

// 另一个文件
const update = useUpdate()

直接调用 update 就可以触发组件的重新渲染了。这个是非常简单的实现,但是非常的实用。后来我自己跑了简单的demo,确实是这样的,厉害呀

hooks 依赖比较

如果依赖项变化了,就会重新请求。在react官方文档中,在强调依赖项是使用的Object.is 进行比较的,在这里也是这样的,源码如下

import type { DependencyList } from 'react';

export const 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

}

数据驱动视图变化,类似vue3的reactive,结合proxy

曾经有一个前辈跟我说,react的底层实现比vue3强多了。react想要实现数据驱动视图变化,其实也很简单。那时候的我,刚入门react没多久,对这句话,百思不得其解。后来看了ahooks的源码,才恍然大悟,原来是这样的。其实就是利用了proxy结合前面说的update,实现数据驱动视图变化。

源码如下

/** 原对象: */
const proxyMap = new WeakMap();
/** 代理过的对象 */
const rawMap = new WeakMap();

function observe<T extends Record<string, any>>(initialVal: T, cd: () => void): T {
  const existingProxy = proxyMap.get(initialVal);
  /** 添加缓存,防止重新构建proxy */
  if (existingProxy) {
    return existingProxy;
  }
  if (rawMap.get(initialVal)) {
    return initialVal;
  }
  const proxy = new Proxy(initialVal, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      const descriptor = Object.getOwnPropertyDescriptor(target, key);
      if (!descriptor?.configurable && !descriptor?.writable) {
        return res;
      }
      return typeof res === 'object' || Array.isArray(res) ? observe(res, cd) : res;
    },
    set(target, key, value) {
      const ret = Reflect.set(target, key, value);
      cd();
      return ret
    },
    deleteProperty(target, key) {
      const ret = Reflect.deleteProperty(target, key);
      cd();
      return ret
    }
  })
  proxyMap.set(initialVal, proxy);
  rawMap.set(proxy, initialVal);
  return proxy;
}

function useReactive<T extends Record<string, any>>(initialVal: T): T {
  const [, setState] = useState({});
  const update = useCallback(() => setState({}), []);
  const stateRef = useRef<T>(initialVal);
  const state = observe(stateRef.current, () => {
    setState({});
  });
  return state;
}

生命周期

我们常说useEffect 等同于三个生命周期:初始化、更新、卸载。ahooks 就很巧妙的把他们拆份开来,分别封装成了useMount、useUpdateEffect、useUnmount。这样就更加清晰了,代码如下

// useUpdateEffect
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);
  // 第一个 hook 调用是为了处理组件卸载时将 isMounted.current 重置为 false,这对于 React Refresh 非常重要。
  hook(() => {
    return () => {
      isMounted.current = false
    }
  })
  // 第二个 hook 调用中,如果 isMounted.current 为 false(即组件首次渲染时),将其设为 true。
  // 否则(即不是首次渲染),便执行传入的 effect 函数,因而实现了仅在依赖项更新时执行效果的功能
  hook(() => {
    if (!isMounted.current) {
      isMounted.current = true
    } else {
      return effect()
    }
  }, deps)
}

import { useEffect } from 'react';

const useUpdateEffect = createUpdateEffect(useEffect);

// useMount

const useMount = (fn: () => void) => {
  useEffect(() => {
    fn?.();
  }, []);
};

// useUnmount
const useUnmount = (fn: () => void) => {
  if (isDev) {
    if (!isFunction(fn)) {
      console.error(`useUnmount expected parameter is a function, got ${typeof fn}`);
    }
  }

  const fnRef = useLatest(fn);

  useEffect(
    () => () => {
      fnRef.current();
    },
    [],
  );
};

export default useUnmount;

// useLatest 包装成ref
function useLatest<T>(value: T) {
  const ref = useRef(value);
  ref.current = value;

  return ref;
}

在技术的历史长河中,虽然我们素未谋面,却已相识已久,很微妙也很知足。互联网让世界变得更小,你我之间更近。

在逝去的青葱岁月中,虽然我们未曾相遇,却共同经历着一样的情愫。谁的青春不曾迷茫或焦虑亦是无奈,谁不曾年少过

在未来的日子里,让我们共享好的文章,共同学习进步。有不错的文章记得分享给我,我不会写好的文章,所以我只能做一个搬运工

我叫 sunseekers(张敏) ,千千万万个张敏与你同在,18年电子商务专业毕业,毕业后在前端搬砖

如果喜欢我的话,恰巧我也喜欢你的话,让我们手拉手,肩并肩共同前行,相互学习,互相鼓励

文档信息

Search

    Table of Contents