最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 聊聊 React Hooks 中的那些心智负担

    正文概述 掘金(SeasonChen)   2021-06-28   626

    前段时间跟几个做 React 的前端朋友谈及 React Hooks 中的心智负担问题,得到的答案让我很是惊讶,因为他们说没有感受到 React Hooks 带来的心智负担。

    我突然就有点自闭了?难道是我自己的问题?

    不得不说,React Hooks 的出现的确给 React 开发者带来了很多方便。但是在实际的使用过程中,我也的确发现它在带来一系列方便的同时,也带来了很多令我困扰以及不爽的地方。

    这种感觉,在使用 React 其他相关的轮子(Redux、Mobox、Dva等等)时也经常遇到,那就是要么是隔靴搔痒只解决问题的一部分,要么是在解决一个问题的同时,又会引入新的问题,总之就是不让你爽的干净利落。

    下面我就介绍一下我在实际项目中使用 React Hooks 遇到的那些心智负担,供大家参考指正。

    陷阱一:引用旧的变量

    先来看一个简单的 Hooks 使用例子。

    function Counter() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        const id = setInterval(() => {
          setCount(count + 1);
        }, 1000);
        return () => clearInterval(id);
      }, []);
    
      return <h1>{count}</h1>;
    }
    

    实现的功能呢很简单,组件安装的时候注册一个定时器,每秒加1,然后组件卸载的时候清除定时器。

    乍一看没问题,但是实际上呢,页面显示的始终是1。这是因为组件每次渲染时,都会创建一个新的变量 count,但是 useEffect 里面的函数是一个闭包,里面引用的 count 变量始终都是第一次渲染时的变量(值始终为1)。

    如果只是解决这个问题,倒是很简单,使用状态改变的函数式调用Api即可:把setCount(count + 1)改成setCount(count => count + 1)

    但是如果 useEffect 里面的函数稍微复杂一点,引用的是多个状态互相依赖,就没法使用这种方式了:

    function Counter() {
      const [count, setCount] = useState(0);
      const [varA, setVarA] = useState('');
      
    
      useEffect(() => {
        const id = setInterval(() => {
            if (varA === 'xxx') {
                setCount(count + 1);
            } else {
                setCount(count + 2);
            }
        }, 1000);
        return () => clearInterval(id);
      }, []);
    
      //...
    }
    

    那该怎么办呢?

    将 useState 换成 useRef?倒是能解决变量引用的问题,但是 useRef 有个问题,就是改变 ref 的值组件并不会重新渲染,界面也不会更新,pass!

    当然还是有解决办法的。

    第一种,使用 useReducer,这里不作示例了, 这里的问题倒是能解决,但是如果 dispatch 带参数的话又回到了问题本身。

    第二种,使用 useEffect 的依赖数组:

    function Counter() {
      const [count, setCount] = useState(0);
      const [varA, setVarA] = useState('');
      
    
      useEffect(() => {
        const id = setInterval(() => {
            if (varA === 'xxx') {
                setCount(count + 1);
            } else {
                setCount(count + 2);
            }
        }, 1000);
        return () => clearInterval(id);
      }, [count, varA]);
    
      //...
    }
    

    但是这种方式有个新的问题,就是 count 和 varA 的每一次改变,定时函数都会重新创建一次,在这个例子里勉强能用,但是在有些场景就有问题了,后面讨论。

    陷阱二:useEffect 到底加不加依赖?

    import { showTip } from 'tip';
    
    function ExampleTip() {
        const [varA, setVarA] = useState('');
        const [varB, setVarB] = useState('');
        
        useEffect(() => {
            showTip({ a: varA, b: varB })
        }, [varA]);
    }
    

    上面这类的需求,其实在开发中很常见,那就是某一个状态(varA)改变时,我们需要做某件事(这里的例子是弹出一个tip框),但是做这件事又需要引用其他的状态(varB等)。

    但是 varB 改变时,我们不应该弹出 tip 框。

    这时候,varB 是不应该加入依赖数组的。


    另一种情况:

    import { fetch } from 'api';
    
    function Example() {
        const [varA, setVarA] = useState('');
        const [varB, setVarB] = useState('');
        
        useEffect(() => {
            window.addEventListener('click', () => {
                doSomeThing({ a: varA, b: varB }) 
            });
            return () => {
                // remove listener
            }
        }, [varA,varB]);
    }
    

    在这里,useEffect 里,对于varB我们想使用的不是useEffect调用时的值,而是varB最新的值。但是,useEffect里引用的变量始终是varA改变时的值,varB改变之后 useEffect 里并不知道。

    这时候,我们就需要把 varB 也加入依赖数组,varB改变时重新监听。


    对于很简单的代码,我们一般能够清楚哪个该加哪个不该加。但是每次编写的时候我们都要考虑哪些该加哪些不该加,无疑加重了心智负担。

    而且,如果没有工具的保证,就会很容易出错,这也是为什么我们需要 eslint、typescript 等工具的原因。

    官方也意识到了这个问题,为我们提供了相应的 eslint 插件:eslint-plugin-react-hooks。

    但现实问题是,对于任何一个状态变量,存在着该加入依赖数组和不该加入依赖数组两种情况,并且这两种情况,工具是没法从代码层面进行区分的!

    怎么办呢?官方的建议是都开启exhaustive-deps规则,也就是说只要是在 useEffect 中引用到的状态变量(除开ref),都应该加入依赖数组!

    官方文档里也说,未来版本,或许会在构建时自动加入第二个参数。

    这意味着,将所有 useEffect 里使用到的变量都加入依赖数组,似乎是官方推荐的、最正确的选择。而且你想要使用 eslint 相关工具的话,也要使用这种方式。

    这解决了引用旧变量的问题,让我们不用去思考该不该加入依赖数组的问题.

    但又引入了新的问题,比如上面showTip那个,我们不得不加入一些额外的代码:

    import { showTip } from 'tip';
    
    function ExampleTip() {
        const [varA, setVarA] = useState('');
        const [varB, setVarB] = useState('');
        
        const varBRef = useRef(varB);
        varBRef.current = varB;
        
        useEffect(() => {
            showTip({ a: varA, b: varBRef.current })
        }, [varA]);
    }
    

    useRef 变量不必要加入依赖数组,因为引用不变。

    但是如果 useEffect 里引用的变量有很多个呢?每一个都加一个ref岂不是很麻烦?

    我一般使用这种方法:

    function ExampleTip() {
        const [varA, setVarA] = useState('');
        const [varB, setVarB] = useState('');
        
        const showTipRef = useRef(null);
        showTipRef.current = () => {
            showTip({ a: varA, b: varBRef.current });
        }
        
        useEffect(() => {
            showTipRef.current && showTipRef.current();
        }, [varA]);
    }
    

    问题是解决了,但是怎么也不爽。

    陷阱三:引用到底变没变?

    对于同一个组件里,我们定义的 useRef 变量可以不加入依赖数组,因为 eslint 插件能识别出这是一个不变的引用。

    但是,看下面这个组件:

    function Example(props) {
        const { id, fetch } = props;
        
        useEffect(() => {
            fetch(id);
        }, [id, fetch]);
    }
    

    我们需要在id改变的时候,执行fetch方法。但是 fetch 引用改变的时候,我们一般是不需要重新 fetch 的。

    即使传入的 fetch 引用是不变的,但是 eslint 插件并不能识别出来,所以它还是要求将 fetch 加入依赖数组。

    这样,乍一看这个组件,你能区分出 fetch 会不会变吗?你能保证只有在 id 改变的时候 fetch 吗?

    看起来是能跑,但是怎么看怎么不放心……

    陷阱四:依赖引入的死循环风险

    function Child(props) {
        const { onAppear, onLeave } = props;
        useEffect(() => {
            onAppear();
            return () => {
                onLeave();
            }
        }, [onAppear, onLeave])
    }
    
    function Parent() {
        const [count, setCount] = useState(0);
        const appearItem = () => {
            setCount(count + 1);
        }
        const leaveItem = () => {
            setCount(count - 1);
        }
        
        return (
            <>
                <Child onAppear={appearItem} onLeave={leaveItem} />
                <Child onAppear={appearItem} onLeave={leaveItem} />
            </>
        );
    }
    

    上面这个例子,单独开每个组件似乎没有问题,但是跑起来就会死循环。因为 Child 组件里的 onAppear, onLeave 引用每次渲染都会变!

    为了解决这个问题,我们不得不将 appearItem, leaveItem通过 useCallback 包裹起来:

    function Child(props) {
        const { onAppear, onLeave } = props;
        useEffect(() => {
            onAppear();
            return () => {
                onLeave();
            }
        }, [onAppear, onLeave])
    }
    
    function Parent() {
        const [count, setCount] = useState(0);
        const appearItem = useCallback(() => {
            setCount(count + 1);
        }, [count]);
        const leaveItem = useCallback(() => {
            setCount(count - 1);
        }, [count]);
        
        return (
            <>
                <Child onAppear={appearItem} onLeave={leaveItem} />
                <Child onAppear={appearItem} onLeave={leaveItem} />
            </>
        );
    }
    

    看似解决了,但还是会死循环,因为 count 每次渲染都会变!这时候又不得不上 useRef 大法了……

    这种隐形的死循环风险,其实在 hooks 代码中很常见。为了规避这些风险,我们不得不想很多。

    陷阱五:误导性的参数

    function Component(props) {
        const varA = useRef(props.a);
        const [varB] = useState(props.b);
    }
    

    上面这个简单的组件,又有什么问题呢?

    乍一眼看去,varA,varB的值是根据props的值动态计算的,因为每次渲染 useRef,useState 都会调用一次,props.a 和 props.b 都会传入一次。

    但是事实上,React 只会使用第一次传入的值!

    这对 useRef, useState 这样经常用的 hooks 函数也许影响不大,因为你一直在注意,都形成了本能,知道它的运行方式了。

    但是对于一个自定义hooks来说呢?

    function Component() {
        const [value, setValue] = useState('');
        const test = useCustomHook(value);
    }
    

    这个vluae,到底是第一个值会被使用,还是所有的值都会被使用呢?

    总结

    写的好累!上面描述的这些例子,都是我在实际工作中经常会遇到的问题。除了这些,还有其他一些问题,只不过很多都不是三言两语能够描述清楚的。

    这只是根据我的水平所得到的理解,其中有些也许不是特别正确,欢迎批评指正!

    总之,给我个人的感觉就是,React Hooks 给我不爽的主要有以下几点:

    • 违背直觉:代码实际的运行方式和你第一眼看上去的感觉差别很大。
    • 需要想很多:很多时候,我们不得不去考虑一些本来不该我们考虑、而应该是框架层面解决的问题。
    • 不安全感:即使你花很多心思写出来的组件,很多时候回头看写下的代码时,总是觉得哪里不对,不是很有安全感。

    仔细寻思一下,其实所有问题的根源,是 React 函数时组件机制所限:每次组件渲染,组件里的所有代码都会被重新调用一次。

    而 Vue 的组合式Api 和 React hooks 如此类似,但是它之所以没有这么多烦恼,主要是因为它的 setup 只会在整个组件的生命周期内执行一次。


    起源地下载网 » 聊聊 React Hooks 中的那些心智负担

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元