「✍ useCallback和usePersistFn」

1,297 阅读3分钟

前言

编写hook的时候,我们总需要编写一大堆useCallback来保证子组件的渲染性能,而useCallback又要考虑一堆的依赖项,某些场景下我们可能需要一个替代方案

usePersistFn

这是umi-hooks中一种持久保存function引用的的方案

import { useRef } from 'react'

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

const usePersistFn = <T extends noop>(fn: T) => {
 const fnRef = useRef<T>(fn)
 fnRef.current = fn

 const persistFn = useRef<T>()
 if (!persistFn.current) {
   persistFn.current = function (...args) { // 永远指向这个外层函数,变化的只是里面的闭包
     return fnRef.current.apply(this, args)
   } as T
 }

 return persistFn.current
}

可以看到usePersistFnpersistFn ref永远指向同一个外层函数,因此引用是不变的,也有下面这种写法

import { useCallback, useRef } from "react";

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

const usePersistFn = <T extends noop>(fn: T) => {
  const fnRef = useRef<T>();
  fnRef.current = fn;

  const presisfn = useCallback((...args) => {
    return fnRef.current.apply(this, args);
  }, []);

  return presisfn;
};

export default usePersistFn;

(在摆脱useCallback的方案中使用useCallback,仅仅只是方便理解 ~)

如何使用:

import React, { useEffect, useState } from "react";
import usePersistFn from "../utils/usePersistFn";
const Index = () => {
  const [count, setCount] = useState(0);
  const [otherState, setOtherState] = useState(0);

  const add = () => setCount((c) => c + 1 + otherState);

  const addPersist = usePersistFn(add);

  useEffect(() => {
    setTimeout(() => {
      setOtherState(10);
    }, 1000);
  }, []);

  return (
    <div>
      <h3>{otherState}</h3>
      <h3>{count}</h3>
        <Child addPersist={addPersist}></Child>
    </div>
  );
};

const Child = React.memo((props: any) => {
  const { addPersist } = props;
  return (
    <button type="button" onClick={addPersist}>
      add count
    </button>
  );
});
export default Index;

我们会发现我们不需要给addPersist加上依赖项count otherState,计算渲染的结果中也能拿到最新的state, 同时addPersist引用一直不会改变,Child组件也不会重复渲染

场景应用

以一个简单的「整数倒计时」useCountDown为例, 想一想这样的hook需要哪些参数呢?我认为最少需要:

startCount : 倒计时开始的数字

endCount :倒计时结束的数字

onStart :开始倒计时的回调

onEnd : 结束倒计时的回调

autoRun: 是否自动运行

我们需要担心的是

startCountendCount并不是同步传递的,可能是某个请求拿到的数据,监听到startCount改变,应该重新倒计时,监听到endCount改变,应该检查比较当前计数和endCount

autoRun 这个参数我认为只需要接收一次,一个倒计时是否自动运行应该在执行前就决定且不能更改,因此我们需要用ref存储

只考虑startCount endCount autoRun , 我们可以写成这样

const useCountDown = (props: IProps): IRes => {
  const { startCount, endCount = 0, autoRun = true } = props

  const timer = useRef(null)
  const autoRunRef = useRef(autoRun) // 用ref只保存一次
  const excute = useCallback(() => {
    setCount(startCount)
    clearInterval(timer.current)
    
    setCount(c => c - 1) // 先执行一次
    timer.current = setInterval(() => {
      setCount(c => c - 1)
    }, 1000)
  }, [onStartPersist, startCount])
  
  useEffect(() => {
    if (autoRunRef.current) {
      excute()
    }
  }, [excute])
  
  /**
   *  清除timer
   */
  useEffect(() => () => clearInterval(timer.current), [])
  
  return { count , excute}
}

但是!

onStartonEnd 就不太寻常了,你不能确定该函数的引用不变,比如开发者这样定义

const onEnd = useCallback(() => {
    console.log(`onstart with count${ count }`)
}, [count])

我们希望这两个function即使变化,也不会触发我们重新运行倒计时,这里就可以用usePersisFn了 !

完整代码

import React, { useCallback, useEffect, useRef, useState } from 'react'
import usePersistFn from './usePresistFn'

interface IProps {
  startCount: number
  endCount?: number
  onStart?: () => void
  onEnd?: () => void
  autoRun?: boolean
}

type IStatus = 'ready' | 'counting' | 'end'
interface IRes {
  count: number
  excute: () => void
  status: IStatus
}
const isFunction = (fn: unknown) => typeof fn === 'function'
const useCountDown = (props: IProps): IRes => {
  const { startCount, endCount = 0, onStart, onEnd, autoRun = false } = props

  const [status, setStatus] = useState<IStatus>('ready')
  // 计时器
  const timer = useRef(null)

  // 是否自动运行
  const autoRunRef = useRef(autoRun)

  // 计数
  const [count, setCount] = useState(startCount)

  const onStartPersist = usePersistFn(() => {
    if (isFunction(onStart)) {
      onStart()
    }
  })

  const onEndPersist = usePersistFn(() => {
    setStatus('end')
    if (isFunction(onEnd)) {
      onEnd()
    }
  })

  useEffect(() => {
    if (count <= endCount) {
      onEndPersist()
      clearInterval(timer.current)
    }
  }, [count, endCount, onEndPersist])

  const excute = useCallback(() => {
    // 初始化
    setStatus('counting')
    setCount(startCount)
    clearInterval(timer.current)

    // 回调
    onStartPersist()

    setCount((c) => c - 1)
    timer.current = setInterval(() => {
      setCount((c) => c - 1)
    }, 1000)
  }, [onStartPersist, startCount])

  useEffect(() => {
    if (autoRunRef.current) {
      excute()
    }
  }, [excute])

  /**
   *  清除timer
   */
  useEffect(() => () => clearInterval(timer.current), [])

  return {
    count,
    excute,
    status,
  }
}

export default useCountDown