ahooks 源码解读系列 - 1

440 阅读10分钟

useUpdateEffect

ahooks官网链接

💡 Tips:忽略首次更新,只有当依赖值发生变化才会执行

场景

有一个需求,当用户的数据发生变更的时候,需要请求后端接口,把用户的变更保存成草稿。

如果使用 useEffect 来实现就会在组件初始化的执行一次不必要的保存操作。

针对这种类似的场景 ahooks 提供了 useUpdateEffect

案例

demo

import React, { useEffect, useState, useRef } from "react";
import { useUpdateEffect } from "./hooks";

export default () => {
  const [count, setCount] = useState(0);
  
   // 两个状态 分别为用effect执行的次数和用useUpdateEffect执行的次数
  const [effectCount, setEffectCount] = useState(0);
  const [updateEffectCount, setUpdateEffectCount] = useState(0);

  useEffect(() => {
    setEffectCount((c) => c + 1);
  }, [count]);

  useUpdateEffect(() => {
    setUpdateEffectCount((c) => c + 1);
  }, [count]);

  return (
    <div>
      <p>effectCount: {effectCount}</p>
      <p>updateEffectCount: {updateEffectCount}</p>
      <p>
        <button type="button" onClick={() => setCount((c) => c + 1)}>
          reRender
        </button>
      </p>
    </div>
  );
};

主要思路

  1. 声明一个hooks用来记录是否是首次渲染
  2. 如果不是首次渲染,才去执行effect

核心代码

import { useRef, useEffect } from "react";

// 用来记录是否是首次渲染
export const useFirstMountState = () => {
  // 声明一个 ref,用于记录当前渲染是否是首次渲染
  const isFirst = useRef<boolean>(true);
  
  // 如果是首次渲染 返回true 并且让 isFirst 为false
  if (isFirst.current) {
    isFirst.current = false;
    
    return true;
  }
  
  return isFirst.current;
};


// useUpdataEffect 用法和useEffect一致 
export const useUpdateEffect = (effect, deps) => {
  // 定义 isFirst用来判断是否为第一次渲染
  const isFirst = useFirstMountState();
  
  useEffect(() => {
    // 如果不是第一次 ,则执行effect
    if (!isFirst) {
      // rerurn effect 把effect的返回值作为 此useEffect的返回值,同时执行effect
      return effect();
    }
  }, deps);
};

useEventCallback

参考链接

💡 Tips:可以在callback依赖频繁更新的时候使用,在不重新修改传递给子组件函数指向的情况下 去更新传递给子组件函数 (一般用作性能优化)

场景

有一个需求,当用户在输入框输入内容,旁边有个搜索按钮,进行列表搜索,搜索按钮的事件需要拿到输入框的内容进行搜索

为了做优化我们可能会使用useCallback包裹这个事件函数,但是输入框的内容频繁变化也会频繁生成新的函数

针对这种场景,可以使用useEventCallback来做优化

案例

demo

import React, { memo, useState } from 'react';
import useEventCallback from '../index';

const Input: React.FC<{
  value: string;
  onChange: (value: string, field: string) => void;
  placeholder: string;
}> = memo((props) => {
  console.log('input');
  const { value, onChange, placeholder } = props;
  return (
    <input
      value={value}
      onChange={(e) => onChange(e.target.value, placeholder)}
      placeholder={placeholder}
    />
  );
});

function Form() {
  const [state, updateState] = useState({
    name: '',
    address: '',
    hobby: ''
  });
  const [count, setCount] = useState(1)
  const { name, address, hobby } = state

  const onChange = useEventCallback((value, field) => {
    updateState({
      ...state,
      [field]: value
    })
    console.log(count, 'count')
  }, [count, state])
  return (
    <>
      <button onClick={() => setCount(pre => pre + 1)}>点我count + 1</button>
      <div>count: {count}</div>
      <hr />
      <Input value={name} onChange={onChange} placeholder='name' />
      <Input value={address} onChange={onChange} placeholder='address' />
      <Input value={hobby} onChange={onChange} placeholder='hobby' />
    </>
  );
}

export default Form;

主要思路

利用ref在整个组件的生命周期内引用不变的特性实现

  1. 创建一个 ref 对象,让 ref.current 等于每次传递过来的 fn
  2. 将 useEffect 的依赖改为 传递过来的依赖 在 useEffect 中修改 ref.current
  3. 创建一个 useCallback 在此 useCallback 中调用 ref.current, 并将此 callback 返回出去

核心代码

import { useCallback, useRef, useEffect } from 'react';

const useEventCallback = (fn, dependencies) => {
  // 创建一个ref对象
  const ref = useRef(() => {
    throw new Error('Cannot call an event handler while rendering.');
  });

  // 当依赖发生更新时 更新ref.current为最新的fn
  // ref的更新不会触发render
  useEffect(() => {
    ref.current = fn;
  }, [fn, ...dependencies]);

  /**
   * 利用ref在组件整个生命周期内引用不变的特性
   * 所以这个callback只会执行一次
   * 但是它内部执行的函数是最新的fn
   */
  return useCallback((...args) => {
    const fn = ref.current;
    return fn.apply(this, args);
  }, [ref]);
};

export default useEventCallback;

usePersistFn

💡 Tips:得到一个引用永远不变的函数,可以在引用不变的情况下获取函数内的最新的值。

场景

有一个场景为倒计时时间,旁边有暂停和开始,每次触发暂停开始操作的时候都需要拿到当前倒计时的时间来继续操作

useCallback针对于这种频繁变化依赖的场景达不到理想的优化效果,ahooks提供了usePersistFn 可以解决这个问题

案例

demo

import React, { memo, useState } from 'react';
import usePersistFn from '../index';

const Submit: React.FC<{
  onSubmit: () => void;
}> = memo((props) => {
  console.log('submit');
  const { onSubmit } = props;
  return <button onClick={onSubmit}>submit</button>;
});

const Test = () => {
  const [inputVal, setInputVal] = useState<string>('');

  const onSubmit = usePersistFn(() => {
    alert(inputVal);
  });

  return (
    <>
      <input type="text" onChange={(e) => setInputVal(e.target.value)} />
      <Submit onSubmit={onSubmit} />
    </>
  );
};

export default Test;

主要思路

和 usEventCallbak 几乎一致,不同的是 useEventCallback 返回的是 callback 需要设置依赖值, usePersistFn 返回的 callback 不需要设置依赖值。

核心代码

import { useRef } from 'react';

export type noop = (...args: any[]) => any;

const usePersistFn = <T extends noop>(fn: T) => {
  const fnCallback = useRef<T>();
  fnCallback.current = fn; // 永远保证 fnCallback.current 是最新的

  // 声明一个需要返回的函数 在这个函数里面调用fnCallback.current
  const persistFn = useRef<T>();

  // 首次进来,设置persistFn,persistFn永远为这个函数地址不会变
  if (!persistFn.current) {
    persistFn.current = function (...args) {
      /**
       * 调用fnCallback, 并且传入参数
       *
       * 当确定前面的变量不为空时  可以使用 ! 运算符
       */
      return fnCallback.current!.apply(this, args);
    } as T;
  }
  return persistFn.current;
};

export default usePersistFn;

useCreation

ahooks官网链接

💡 Tips:useCreation 是 useMemo 或 useRef 的替代品。

而相比于 useRef,你可以使用 useCreation 创建一些常量,这些常量和 useRef 创建出来的 ref 有很多使用场景上的相似,但对于复杂常量的创建,useRef 却容易出现潜在的性能隐患。

const a = useRef(new Subject()) // 每次重渲染,都会执行实例化 Subject 的过程,即便这个实例立刻就被扔掉了
const b = useCreation(() => new Subject(), []) // 通过 factory 函数,可以避免性能隐患

场景

如果我们需要使用 useRef 来创建一个常量,但是这个 useRef 初始化给的值需要一系列操作才能得到 传统的 useRef 虽然可以做到,但是 传统的 useRef 每次组件重新渲染的时候的时候都会执行初始化的操作 ,比如实例化一个内容,即便这个实例立刻就被扔掉了,这样会导致不必要的性能的问题。

而 useCreation 只会在第一次初始化的时候执行一次操作,以后就不会再执行了。

案例

demo

import React, { useState } from 'react';
import useCreation from '../index';

class Foo {
  constructor() {
    console.log('Foo created');
    this.data = Math.random();
  }

  data: number;
}

export default function () {
  const foo = useCreation(() => new Foo(), []);
  const [, setFlag] = useState({});
  return (
    <>
      <p>{foo.data}</p>
      <button
        type="button"
        onClick={() => {
          // 强制更新页面
          setFlag({});
        }}
      >
        Rerender
      </button>
    </>
  );
}

核心代码

import { useRef } from 'react';
import type { DependencyList } from 'react';
import depsAreSame from '../utils/depsAreSame';

function depsAreSame(oldDeps: DependencyList, deps: DependencyList): boolean {
  // 如果依赖没更新 return true
  if (oldDeps === deps) return true;

  // 循环遍历每一个值 是否一样 如果有一个值不一样 则返回false  (只会遍历第一层 不会深度遍历)
  for (let i = 0; i < oldDeps.length; i++) {
    if (!Object.is(oldDeps[i], deps[i])) return false;
  }
  return true;
}



const useCreation = <T>(factory: () => T, deps: DependencyList) => {
  /**
   * 初始化依赖必须为undefined,
   * ref的初始化值虽然只会保留第一次的值,但是后续的初始化他还是会执行这块的初始化 只不过用的第一次初始化的值
   *
   * 所以设置undefined 以保证后续不会多次执行factory
   */
  const ref = useRef({
    deps,
    value: undefined as undefined | T,
    initialized: false,
  });

  // 依赖更新了或者第一次进来
  if (ref.current.initialized === false || !depsAreSame(ref.current.deps, deps)) {
    ref.current.deps = deps;
    ref.current.value = factory();
    ref.current.initialized = true;
  }

  return ref.current.value as T;
};

export default useCreation;

useAsyncEffect

ahooks官网链接

💡 Tips:useEffect 不支持异步 async 函数, 理由是 effect 函数应该返回一个销毁函数,如果 useEffect 第一个参数传入 async 函数,返回值则变成了 Promise

场景

比如我们希望在页面渲染完毕后执行一次请求去更改数据,这时候我们可以使用 useAsyncEffect,可以在代码层面上使用async函数, 代码更容易阅读,传统的写法是

useEffect(() => {   
  fetchData().then( res => {
    // ...
  } ) 
}, [])

使用 useAsyncEffect 可以在代码阅读层面上更清楚

useEffect( async () => {
  const res = await fetchData();
  // ...
}, [])

建议使用 useRequest

案例 1

demo

import React, { useState } from 'react';
import useAsyncEffect from '../index';

// 模拟请求
function mockCheck(): Promise<boolean> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(true);
    }, 3000);
  });
}

export default () => {
  const [pass, setPass] = useState<boolean>();

  useAsyncEffect(async () => {
    // 用请求来的数据更改pass
    setPass(await mockCheck());
  }, []);

  return (
    <div>
      {pass === undefined && 'Checking...'}
      {pass === true && 'Check passed.'}
    </div>
  );
};

案例 2

demo

import React, { useState } from 'react';
import useAsyncEffect from '../index';

function mockCheck(val: string): Promise<boolean> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(val.length > 2);
    }, 1000);
  });
}

export default () => {
  const [value, setValue] = useState('');
  const [pass, setPass] = useState<boolean>();

  useAsyncEffect(
    async function* () {
      // 执行函数开始修改pass为undefined
      setPass(undefined);
      const result = await mockCheck(value);
      yield; // Check whether the effect is still valid, if it is has been cleaned up, stop at here.
      // 执行完赋值
      setPass(result);
    },
    [value],
  );

  return (
    <div>
      <input
        value={value}
        onChange={(e) => {
          setValue(e.target.value);
        }}
      />
      <p>
        {pass === undefined && 'Checking...'}
        {pass === false && 'Check failed.'}
        {pass === true && 'Check passed.'}
      </p>
    </div>
  );
};

主要思路

  1. 首先声明一个函数判断是否为 asyncGenerator
  2. 在 useEffect 里调用 传递过来的 fn
  3. 声明一个变量表示组件的状态
  4. 判断是否是 asyncGenerator 如果是,则循环调用 next 方法,直到组件的每一行代码都会执行完成
  5. 判断如果组件卸载或者 next 返回值的状态为 true 表示函数执行完毕
  6. 卸载阶段更新组件的状态

核心代码

import type { DependencyList } from 'react';
import { useEffect } from 'react';

const useAsyncEffect = (
  effect: () => AsyncGenerator<void, void, void> | Promise<void>,
  deps: DependencyList,
) => {
  // 判断是否为generator函数调用后的结果
  function isGenerator(
    val: AsyncGenerator<void, void, void> | Promise<void>,
  ): val is AsyncGenerator<void, void, void> {
    // generator函数调用后的结果有Symbol.asyncIterator属性为函数
    return typeof val[Symbol.asyncIterator] === 'function';
  }

  useEffect(() => {
    // 调用异步函数
    const e = effect();
    // 声明一个变量,用于保存组件是否被销毁
    let cancelled = false;

    // 声明一个异步函数
    async function execute() {
      // 判断effect是否是generator函数
      if (isGenerator(e)) {
        // 如果是generator函数
        while (true) {
          // 循环调用next方法
          const { done } = await e.next();

          // 如果done为true 则表示函数执行完毕 或者 组件卸载了,退出循环
          if (done || cancelled) {
            break;
          }
        }
      } else {
        // 如果不是generator函数 正常await e
        await e;
      }
    }

    // 调用execute函数
    execute();

    return () => {
      // 组件卸载阶段更新cancelled为true
      cancelled = true;
    };
  }, deps);
};

export default useAsyncEffect;

useEventEmitter

ahooks官网链接

💡 Tips:可以实现多个组件之间的共享通讯

场景

比如 有两个组件需要做联动效果,两个组件是兄弟组件,当组件 A 的值发生变化时,组件 B 的值也会发生变化, 传统的props传递方式,写起来会很麻烦,而且会出现很多不必要的重复代码

这种联动效果可以通过 useEventEmitter 实现。父组件把 useEventEmitter 实例传递给子组件 子组件就可以通过实例去进行发布订阅通信,从而达到联动效果。

案例

demo

import React, { FC, memo, useState, useEffect } from 'react';
import { useUpdateEffect } from 'ahooks';
import useEventEmitter, { EventEmitter } from '../index';

const Count: FC<{
  emitter: EventEmitter<number>;
}> = memo(function (props) {
  const [count, setCount] = useState(0);

  useUpdateEffect(() => {
    props.emitter.emit(count);
  }, [count]);
  return (
    <div style={{ paddingBottom: 24 }}>
      count: {count}
      <button
        type="button"
        onClick={() => {
          setCount(count + 1);
        }}
      >
        count + 1
      </button>
    </div>
  );
});

const MyCount: FC<{
  emitter: EventEmitter<number>;
}> = memo(function (props) {
  console.log('子组件执行了');

  const [count, setCount] = useState(0);
  const [myCount, setMyCount] = useState(0);

  props.emitter.useSubscription(() => {
    setCount(count + 1);
    console.log('myCount', myCount);
  });
  return (
    <div>
      myCount : {myCount}
      count: {count}
      <button onClick={() => setMyCount(myCount + 1)}>myCount + 1 </button>
    </div>
  );
});

export default function () {
  const [state, setState] = useState<boolean>();
  const emitter = useEventEmitter<number>();

  return (
    <>
      <div>
        <button onClick={() => setState(!state)}>toggle state</button>
      </div>
      -------------Count-------------
      <Count emitter={emitter} />
      ------------------MyCount------------------
      <MyCount emitter={emitter} />
    </>
  );
}

主要思路

  1. 定义容器来收集所有的订阅者
  2. 定义一个发布者
  3. 定义订阅者,在组件卸载时移除该组件内订阅者
  4. useEventEmitter时保证不会重复生成EventEmitter实例

核心代码

import { useEffect, useRef } from "react";

export type SubScription<T> = (val: T) => void;

export class EventEmitter<T> {
  // 定义一个容器去装载所有的订阅者
  private subscriptions = new Set<SubScription<T>>();
  
  // 发布消息 在这里调用了所有的订阅函数
  emit = (val: T) => {
    this.subscriptions.forEach((subscription) => subscription(val));
  };
  
  // 往订阅容器里去装载订阅者,在组件卸载阶段会删除该订阅者
  useSubscription = (callback: SubScription<T>) => {
    
    // 这里使用 useRef去包裹callback 以保证callback在最终调用时为最新的函数
    const callbackRef = useRef<SubScription<T>>(callback);
    // 更新callbackRef中的callback以保证为最新函数
    callbackRef.current = callback;
    
    useEffect(() => {
      /**
       * 当emit执行的时候 会调用这个函数 在这个函数里调用了订阅者的函数
       * 因为外部 callbackRef.current 永远都是最新的 ,所以当此函数执行时
       * 顺着作用域往上找 找到的函数永远为最新的函数
       *
       * 这里利用了useRef在组件的生命周期内引用保持不变 ,所以每次调用的时候永远为最新的函数
       * @param val 传递进来的值
       */
      function subscription(val: T) {
        // 如果callback有值调用callback
        if (callbackRef.current) {
          callbackRef.current(val);
        }
      }
      // 将此订阅着装载到容器中
      this.subscriptions.add(subscription);
      return () => {
        // 组件卸载阶段删除该订阅者
        this.subscriptions.delete(subscription);
      };
    }, []);
  };
}

export const useEventEmitter = <T>() => {
  const ref = useRef<EventEmitter<T>>();
 /**
   * 用ref来记录当前的发布订阅实例
   * 如果是首次进来 则会创建一个新的发布订阅实例
   * 如果不是首次进来 则会返回之前创建的发布订阅实例
   *
   * 这里是必要的  如果不这样的话 则会每次调用的时候会生成一个新的函数
   * 每次都会给子组件传递一个新的发布订阅实例
   *
   * 利用了useRef在组件的生命周期内引用保持不变
   */
  if (!ref.current) {
    ref.current = new EventEmitter<T>();
  }
  return ref.current;
};

最后

上述的hooks很多用到的都是ref的特性

保存任何可变的值

useRef 返回一个可变的 ref 对象,ref 对象在组件的整个生命周期内持续存在,

useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象

所以 所有的对ref的赋值或者取值拿到的都是最新的状态