本文是在:https://juejin.im/post/5ceb36dd51882530be7b1585 的基础上进行的探究,非常建议阅读原文

一、useEffect 依赖诚实问题的粗暴解决及带来的问题

之前的一个例子,在 useEffect 中直接执行 setInterval 导致依赖欺骗带来的很多问题,详细的内容请移步至:

const [count, setCount] = useState(0);
const [step, setStep] = useState(1);

useEffect(() => {
    console.log('render useEffect')
    const id = setInterval(() => {
        setCount(prevCount => prevCount + step);
        setStep(step => step + 1);
        console.log(`[] count is ${count}, step is ${step}`);
    }, 1000);
    return () => clearInterval(id);
}, [step]);

上面代码中,虽然通过 setStaet(prevState => prevState + 1) 这样的方式取消对 count 的依赖,但是一旦代码里面同时依赖了两个 state,就无法通过这种方式解决。

上面的代码中,最终解决的方案其实是在 useEffect 中依赖了 step,这已经是依赖诚实,但是造成的结果是显而易见的:每次 step 的变动都会导致重新实例化一次 setInterval 。

13285-l0nv0p7q4ul.png

二、使用 useReducer 解决依赖诚实问题

我们最终的目的是 useEffect 本身的依赖只有 [ ],以为只有 [ ] 我们才能保证组件实例挂载的时候只会执行一次 setInterval

首先我们的依赖关系是发生在 useState 上的(具体的是 setCount),如果能够解决 setCount 中本身对 count 和 step 的依赖关系是最好的。

而 react 的文档中,明确提出了 useReduceruseState 的替代方案

文档原文:

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等

从这个介绍上来看,使用 useReducer 在上面的场景中是比 useState 更合适的。

useReducer hook 是 react 的内置 hook,在声明上如下:

const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer 本身接受的参数有三个:

  • 一个 reducer:(state, action) => newStat(reducer 与 redux 中的概念其实一样)
  • 初始化值
  • init 是用来进行惰性初始化的:init(initialArg)

如果使用 redux ,本身我们就不会直接操作 state,而是通过 dispatch 去触发某些规则,因此 useReducer 本身也会返回一个 dispatch

1、声明一个 reducer

下面的 reducer 比较简单,处理了一下 increment

const reducer = (state, action) => {
    switch (action.type) {
        case 'increment':
            return {
                ...state,
                count: state.count + state.step,
                step: state.step + 1,
            }
        default:
            return state;
    }
}

2、使用 useReducer 声明 state 和 dispatch

const initialState = {
    count: 0,
    step: 1
}
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;

3、使用 dispatch 进行 state 的一些变更

一开始提出的代码改成下面:

useEffect(() => {
    console.log('render useEffect')
    const id = setInterval(() => {
        dispatch({ type: 'increment' });
        console.log(`[] count is ${state.count}, step is ${state.step}`);
    }, 1000);
    return () => clearInterval(id);
}, []);

4、效果:

首先我们实现了实例一次 setInterval 定时器,但是却能够时刻处理 count 和 step 的变化

在一次渲染闭包内,能够每次访问到 state 的最新值

64154-uto9p0keipk.png

5、依赖真的都诚实了么?

现在对 count 和 step 的两个依赖都剥离出去了,我们认为目前 useEffect 的依赖都是诚实的,其实不然。

因为我们最终还是依赖了 dispatch,不是只有 state 才叫依赖

但是我们都知道 dispatch 本身是不会变化的,因此我们认为对 uesEffect 来说,依赖都是诚实的

三、useCallback 解决 useEffect 内部函数的依赖诚实问题

1、非 useEffect 内部函数引起的依赖欺骗

上面代码中我们发现,如果 dispatch 内部也依赖了某些变量,这个时候很容易造成依赖的欺骗问题。

为了解决这个问题,我们可能都需将其他函数写在 useEffect 内部才能借助 eslint-plugin-react-hooks 这个插件检查通过

可以针对思考下面代码:

const [count, setCount] = useState(0);
const [step, setStep] = useState(1);

const setCountNew = () => {
    setCount(count + step);
    setStep(step + 1);
}

useEffect(() => {
    const tm = setInterval(() => {
        setCountNew();
        console.log(count, step)
    }, 1000);

    return () => { clearInterval(tm); }
}, []);

上面的代码只是将 setCount 和 setStep 这样的方法移到了 useEffect 外面,目前在 useEffect 中我们从代码上看(忽略 console)是没有 state 的依赖的,看起来是没问题。

eslint 插件只会扫描出 setCountNew()

19773-3csl7ku5vdd.png

而上面的输出结果只会输出一次,即使我们有定时器。定时器是一致在执行的,但是页面是不会变化的,因为每次在 setCountNew 的时候,拿到的 count 和 step 都是第一次渲染闭包的值,也就是 0 和 1

2、useCallback 解决依赖欺骗问题

有些情况下我们不能将函数都写在 useEffect 内部,会造成无法管理,代码也会臃肿。

useCallback 本身会返回一个方法,同时 useCallback 接收两个参数:

  • 参数1:匿名方法,里面执行相关的逻辑
  • 参数2:数据依赖,本身 useCallback 需要监听相关的依赖项,这些依赖项可以在上面的方法中使用

文档的说明:

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新

上面方法的改造如下:

    const [count, setCount] = useState(0);
    const [step, setStep] = useState(1);

    const setCountNew = useCallback(() => {
        setCount(count + step);
        setStep(step + 1);
    }, [count, step]);

    useEffect(() => {
        console.log('render useEffect')
        const tm = setInterval(() => {
            setCountNew();
        }, 1000);

        return () => { clearInterval(tm); }
    }, [setCountNew]);

上面的改动中,除了我们使用 useCallback 声明一个 setCountNew 的方法,并且在 useEffect 方法本身用之外,useEffect 还依赖了 setCountNew

这个表示说明,当 setCountNew 发生变化的时候(本身如果 state 发生了变化则返回的方法也会发生变化)

输出结果:

41524-yop3qbyh0lr.png

我们可以发现,输出结果中,每次都会重新执行 useEffect ,因为对于 useEffect 来说,useCallback 的 memorize 回调已经发生变化,基于此,我们可以放心的认为 useEffect 中依赖都是诚实的。

四、代码

文章已经结束啦