V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
wiluxy
V2EX  ›  React

一个关于 react 函数组件重新渲染的问题

  •  1
     
  •   wiluxy · 2021-04-19 21:11:43 +08:00 · 3420 次点击
    这是一个创建于 1343 天前的主题,其中的信息可能已经有所发展或是发生改变。

    点击按钮后,触发 setCount(0+1), 此时 count 改变,改变后 count 为 1,函数重新渲染 输出"render" 然后再次点击按钮,触发 count+1,但是外面包了一层 useCallback,函数里面的 count 应该是第一次渲染时的 count,也就是 setCount(0+1),第二次点击 connt 是没有变的,但是函数还是重新渲染了,输出了"render",这是为什么呢?

    代码如下

    import React, { useCallback,  useState } from "react";
    
    const App = () => {
    
      console.log("render");
      
      const [count, setCount] = useState(0);
    
      const Add = useCallback(() => {
        setCount(count + 1);
      }, []);
    
      return (
        <div>
          {count}
          <button onClick={Add}>add count</button>
        </div>
      );
    };
    
    export default App;
    
    第 1 条附言  ·  2021-04-20 10:06:34 +08:00
    感觉我的表达能力还是有点问题,有一个情况忘了说,就是第三次点击 add count 后(此时已经输出两次"render"),之后再点击多少次都不会再输出(输出 render 我理解为 count 更新->函数组件重新运行)
    25 条回复    2021-04-21 10:32:14 +08:00
    ericls
        2
    ericls  
       2021-04-19 21:20:49 +08:00 via iPhone
    不管 count 变没变 你 dispatch 了就是 dispatch 了
    huijiewei
        3
    huijiewei  
       2021-04-19 21:28:59 +08:00
    函数组件就是这样的,里面任何状态的变化都会重新运行函数 :)
    wiluxy
        4
    wiluxy  
    OP
       2021-04-19 21:32:01 +08:00
    djyde
        5
    djyde  
       2021-04-19 21:43:29 +08:00
    还有另外一个问题是 setCount(count + 1); 在 callback 里总是初始的 count, 应该用 setCount(count => count + 1)
    wiluxy
        6
    wiluxy  
    OP
       2021-04-19 22:11:46 +08:00
    @djyde 这里是为了让每次点击的时候 setCount 的值是一样才这么做的,setState 的时候会用 Object.is 比较新旧值,一样的话好像就不重新渲染页面了
    aaronlam
        7
    aaronlam  
       2021-04-19 22:54:54 +08:00
    只要调用了 useState 所返回数组里的第二个函数元素,就会重新执行函数组件
    wiluxy
        8
    wiluxy  
    OP
       2021-04-19 23:04:54 +08:00
    @aaronlam 这个例子里面只有前两次点击 add count 会输出“render”,后面再怎么点也不会输出 render 了,count 的值也没有变化
    zhuangzhuang1988
        9
    zhuangzhuang1988  
       2021-04-19 23:33:36 +08:00
    所以 react hook 反人类。。。
    imjamespond2020
        10
    imjamespond2020  
       2021-04-19 23:50:27 +08:00 via Android
    usecallback 的用法是传递到子组件的配合 useeffect 用的。。
    weimo383
        11
    weimo383  
       2021-04-19 23:50:38 +08:00 via Android
    你的 add 函数是一个在 mount 阶段就被缓存的函数,不会重新创建,生成闭包
    weimo383
        12
    weimo383  
       2021-04-19 23:53:27 +08:00 via Android   ❤️ 1
    函数执行并不代表组件的重新渲染。实际上 react 组件返回的是新的虚拟 dom
    shzx1994529
        13
    shzx1994529  
       2021-04-20 01:06:08 +08:00
    函数执行不等于组件渲染,后面可能会被 diff 掉,不用太在意
    ericgui
        14
    ericgui  
       2021-04-20 09:25:26 +08:00
    setCount(count => count + 1);
    dany813
        15
    dany813  
       2021-04-20 10:38:00 +08:00
    @shzx1994529 diff 多了 也挺费劲的
    shzx1994529
        16
    shzx1994529  
       2021-04-20 11:45:45 +08:00
    @dany813 也是,不过正常开发不乱写一般没啥性能问题
    wiluxy
        17
    wiluxy  
    OP
       2021-04-20 11:47:58 +08:00
    @ericgui 并不是要 setCount 能更新,而是 setCount 相同的值能触发函数更新 ,count 为 1 的时候 setCount(1),能触发更重新运行函数组件,但是后续触发又不触发了
    liuqiongyu889
        18
    liuqiongyu889  
       2021-04-20 13:37:39 +08:00
    你的依赖有问题,应该写为:
    const Add = useCallback(() => {
    setCount(count + 1);
    }, [count]);

    不然 count 没有更新,还是原来的引用,可以看下这篇文章,解释 useCallback 和 useMemo 作用,为什么需要这个东东:

    [一句话解释 useCallback 与 useMemo 的区别 & 作用]( https://markdowner.net/article/153901518641561600)
    liuqiongyu889
        19
    liuqiongyu889  
       2021-04-20 13:39:58 +08:00
    如果你不想在依赖列表中写 count,应该参考 @ericgui 的写法,传递一个函数进去:
    setCount(count => count + 1);
    wiluxy
        20
    wiluxy  
    OP
       2021-04-20 13:43:51 +08:00
    @liuqiongyu889 这里是为了复现这个问题才这样写的,故意让 count 是第一次运行时的值( 0 ),第一次点击的时候 count 0->1,第二次点击的时候 count 1->1,理应是不会触发 App 函数重新运行的,结果触发了,但是第三次点击的时候 count 1->1,但是又没有触发运行,疑问点在这,不是 useCallback 的疑问,而是 useState,更新状态函数的疑问。
    liuqiongyu889
        21
    liuqiongyu889  
       2021-04-20 13:51:06 +08:00   ❤️ 1
    @wiluxy 这个我有点不解了,因为你这种做法有点违背 hook 的最佳实践用法,可能得挖挖源码看下喽,React Hook 最佳实践: https://markdowner.net/article/170279075167232000,外国人写的,翻译文。
    SystemLight
        22
    SystemLight  
       2021-04-20 17:19:04 +08:00
    我理解的是把函数式组件看成类组件的 render 方法,里面的 hook 相比类组件的其它定义方法,一旦组件内状态改变就会调用 render 方法去重新渲染生成新的 VDOM,而且是从根节点往下传递的
    wiluxy
        23
    wiluxy  
    OP
       2021-04-20 17:44:57 +08:00
    @SystemLight 已知的是 useState 的修改函数,传入的值新旧比较是用 Object.is 来比较的,如果一样的就不会进行更新

    ~~~js
    const [user,setUser] = useState({
    name:"tim"
    })

    setUser(u=>{
    u.name = "jojo"
    return u
    });
    ~~~
    向上面这样调用 setState 函数是不会触发更新的,但是我的疑问是 帖子内容的代码第一次执行 setCount(1)的时候函数重新渲染了,但是第二次 setCount(1),count 的值没有变化,函数还是重新渲染了,第三次第四五次之后再点又没有出现组件函数重新渲染的行为
    7anshuai
        24
    7anshuai  
       2021-04-20 20:35:59 +08:00
    SystemLight
        25
    SystemLight  
       2021-04-21 10:32:14 +08:00   ❤️ 1
    粗略看了下 react-dom 源码,其中有一部分是这样子的

    ```
    function dispatchAction(fiber, queue, action) {
    ...

    if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
    ...
    if (objectIs(eagerState, currentState)) {
    // Fast path. We can bail out without scheduling React to re-render.
    // It's still possible that we'll need to rebase this update later,
    // if the component re-renders for a different reason and by that
    // time the reducer has changed.
    return;
    }
    }
    ...
    }
    ```

    状态改变后,第二次设置的值的时候,fiber.lanes 的标记会从 NoLanes 变为存在 SyncLane,如果没有同步的话是根本不会进行 objectIs 进行状态比较的,我的理解是 fiber 需要一回做同步,因为无法准确确定出某些事情,所以只能在下回合更新时进行执行判断
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2930 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 13:22 · PVG 21:22 · LAX 05:22 · JFK 08:22
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.