前言
编写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
}
可以看到usePersistFn
让persistFn 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
: 是否自动运行
我们需要担心的是
startCount
和endCount
并不是同步传递的,可能是某个请求拿到的数据,监听到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}
}
但是!
onStart
和 onEnd
就不太寻常了,你不能确定该函数的引用不变,比如开发者这样定义
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