起因

在实践中遇到一个场景,是在useEffect中注册一个事件监听函数,在事件监听的回调函数中,将接收到的数据设置到数组类型的state中,同时提供一个boolean类型的state与页面中的按钮绑定,控制接收和暂停,当设置为暂停时,回调函数中就不更新state,示意如下:

function MyComponent() {
    const [datas, setDatas] = useState<number>([]); // 展示数据
    const [paused, setPause] = useState(false); // 控制是否接受新数据
    
    useEffect(() => {
        const callback = (data: number) => {
            if (!pasued) {
                // 将接收的数据加入到state里
                setDatas(oldDatas => [...oldDatas, data]);
            }
        }
        // GlobalEvent是封装的一个事件,注册一个监听器来接收数据
        GlobalEvent.addMessageListener("eventId", callback);
        return () => {
            GlobalEvent.removeMessageListener("eventId", callback);
        }
    }, []);
    
    return (
        <div>
            <Button onClick={() => setPause(state => !state)}>{paused ? "继续" : "暂停"}</Button>
        <div>
    );
}

这样,直观上开起来没有问题,通过设置pasued的状态来动态控制数据的设置,但是实际运行起来会发现paused并没有起到任何作用,原因是回调函数在此处表现上是一个闭包,其里边拿到的paused变量的地址是paused初始值的地址,当paused的值发生变化时,外部的paused变量指向了新的内存地址,但回调函数内部依旧保持的旧的引用,因此外状态部改变,内部不会生效。

解决

既然是因为变量引用无法更新,那么就可以通过ref来引用,将回调函数与ref相关联,将paused的状态通过ref引用进行传递,当pasued的状态发生变化时,及时更新ref的引用即可,如下:

function MyComponent() {
    const [datas, setDatas] = useState<number>([]); // 展示数据
    const [paused, setPause] = useState(false); // 控制是否接受新数据
    const pausedRef = useRef<boolean>(); // paused的动态引用
    
    const callback = useCallback((data: number) => {
        // 通过ref动态引用获取到paused的当前状态
        if (!pausedRef.current) {
            setDatas(oldDatas => [...oldDatas, data]);
        }
    }, [pausedRef]);
    
    useEffect(() => {
        // GlobalEvent是封装的一个事件
        GlobalEvent.addMessageListener("eventId", callback);
        return () => {
            GlobalEvent.removeMessageListener("eventId", callback);
        }
    }, []);
    
    useEffect(() => {
        // 当paused的值发生变化时更新ref的引用
        pausedRef.currect = pause;
    }, [paused]);
    
    return (
        <div>
            <Button onClick={() => setPause(state => !state)}>{paused ? "继续" : "暂停"}</Button>
        <div>
    );
}