• 使用 useReducer 和 useCallback 解决 useEffect 依赖诚实与方法内置&外置问题


    本文是在: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 的文档中,明确提出了 useReducer 是 useState 的替代方案

    文档原文:

    在某些场景下,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 中依赖都是诚实的。

  • 相关阅读:
    hadoop
    常用安装
    rdd相关
    spark安装
    psutil
    scala linux终端高亮显示
    【原创】正则表达式(知识点总结)
    检测对象是否有某个属性(原型链检测和对象自身检测)
    JavaScript核心(对象+原型+函数+闭包+this+上下文堆栈+作用域链)
    JavaScript ES6迭代器指南
  • 原文地址:https://www.cnblogs.com/fightjianxian/p/12533209.html
Copyright © 2020-2023  润新知