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

react 新手关于 react useEffect 的困惑,为什么 useEffect 里面的 cleanup 函数里面的 props 是旧的,如何从源码解释?

  •  
  •   ooo4 · 2 天前 · 1708 次点击

    版本: [email protected]

    我看源码就是先 UnmountEffects 后 MountEffects,里面也只是递归遍历而已,为什么 cleanup 里面的 props 是上一次的了?

    commitPassiveUnmountEffects(root.current);
    commitPassiveMountEffects(root, root.current, lanes, transitions);
    
    // 复现的 demo
    function App() {
      const [num, setNum] = useState(100)
      window.__setNum = setNum
      return <Comp num={num}></Comp>
    }
    
    function Comp(props) {
      debugger
      useEffect(() => {
        debugger
        props // {num:1000}
        return () => {
          debugger
          // 为什么这里是旧的 props? {num:100}
          props
        }
      }, [props.num])
      return (
        <p>
          <span>{props.num}</span>
        </p>
      )
    }
    
    setTimeout(() => {
      __setNum(1000)
    }, 1000)
    
    44 条回复    2025-01-05 21:35:24 +08:00
    shintendo
        1
    shintendo  
       2 天前   ❤️ 1
    闭包
    lisongeee
        2
    lisongeee  
       2 天前
    新状态组件的 useEffect 和旧状态组件的 cleanup 同时被你 debugger 到了
    ooo4
        3
    ooo4  
    OP
       2 天前
    @shintendo 从 chrome 的 debugger 工具看,这个 props 确实是来源于闭包,thanks!!
    ltaoo1o
        4
    ltaoo1o  
       2 天前
    我这里也有一个闭包的问题,https://codesandbox.io/p/sandbox/zuo-yong-yu-de-wen-ti-kvt3jp?file=%2Fsrc%2FApp.js

    react 的函数组件让人恼火,各种潜在的问题
    Razio
        5
    Razio  
       2 天前
    @ltaoo1o #4 你比 OP 还新手
    ltaoo1o
        6
    ltaoo1o  
       2 天前
    @Razio 可以详细说说吗,不要只输出态度..
    Torpedo
        7
    Torpedo  
       2 天前
    @ltaoo1o #4 就是闭包的问题。每次组件状态变化,函数都会执行一次。所以最新的状态都在最新的一次执行里。但是这里你 keydown 监听的是第一次函数运行的函数。那个函数的闭包上下文里,state 是最初的。

    新手不要直接用 useEffect 。找个 react-use ahook 啥的。用封装好的 hook 。无论什么水平,写的好的话,需要手写 useEffect 的很少
    shintendo
        8
    shintendo  
       2 天前
    @ltaoo1o 你这个我觉得无解,可行的解法你自己注释里都写了
    或者干脆就从 dom 里拿 v
    hyh0u0
        9
    hyh0u0  
       2 天前
    (变量)代码在写下来的时候就被捕获了。或者说,在这个函数的生命周期里,函数内部的那个 props 就只有传入的 props
    TWorldIsNButThis
        10
    TWorldIsNButThis  
       2 天前 via iPhone
    @ltaoo1o 注释是啥意思,正确方式就是 ref 或者让 effect 依赖 v 啊
    ltaoo1o
        11
    ltaoo1o  
       2 天前
    当时出问题的时候,我就意识到是闭包问题了,百分之九十九函数组件的问题就是闭包问题 🤣

    我这个代码,从语义上来说非常简单也很实际,「页面初始化后监听回车事件」,用函数组件就是写不出来,用类组件就没有这种问题。
    所以我现在写代码都少用 hook ,它改变了代码作为领域知识的意义,变成为框架去改变业务含义,导致现在都是在写框架代码,不是写业务代码。
    ltaoo1o
        12
    ltaoo1o  
       2 天前
    @TWorldIsNButThis 我希望代码表达正确的「语义」
    ltaoo1o
        13
    ltaoo1o  
       2 天前
    @shintendo #8 是的,我想了很久,无解
    ltaoo1o
        14
    ltaoo1o  
       2 天前
    @Torpedo 感谢 🙏🏻
    shintendo
        15
    shintendo  
       2 天前
    @ltaoo1o 你说 v 不能放 useRef 里,那可以把 log 放 useRef 里
    https://codesandbox.io/p/sandbox/zuo-yong-yu-de-wen-ti-forked-pn425w
    TWorldIsNButThis
        16
    TWorldIsNButThis  
       2 天前 via iPhone
    @ltaoo1o
    class 组件可以是因为 this 就是 ref 啊
    ltaoo1o
        17
    ltaoo1o  
       2 天前
    @shintendo #15 起作用的是这个刷新 log 函数的代码吧,而且 log 可能是一个比较复杂的函数,它要拿很多个状态进行处理,这里就要依赖 v1 v2 v3 等等,容易漏

    ```
    useEffect(() => {
    logRef.current = () => {
    alert(v);
    };
    }, [v]);
    ```
    ltaoo1o
        18
    ltaoo1o  
       2 天前
    @TWorldIsNButThis #16 所以我说函数组件有额外的心智负担,容易出问题,感觉在和框架斗智斗勇 🤣
    ljpCN
        19
    ljpCN  
       2 天前
    @ltaoo1o 你应该在 input 标签的 onKeyDown 回调里处理你的键盘监听,而不是在 useEffect 里处理
    ltaoo1o
        20
    ltaoo1o  
       2 天前
    @ljpCN 实现方式其实很多种,难的是保留语义的前提下。我希望我的代码,别人一看,就能明白是「当页面加载后,监听回车事件并 xxx 」,如果写在 input 标签,就变成了「当这个 input 回车时,xxx 」,这里的语义就丢失了。
    当然代码能跑就行,「代码表达语义」仅仅是我个人的追求。
    ljpCN
        21
    ljpCN  
       2 天前   ❤️ 1
    @ltaoo1o 那给你看看我改完的代码吧,个人觉得比你的语义更清晰。https://codesandbox.io/p/sandbox/zuo-yong-yu-de-wen-ti-forked-6ljjys
    ltaoo1o
        22
    ltaoo1o  
       2 天前
    @ljpCN #21 额,语义是指业务逻辑,就比如我必须表达出「当页面加载后,监听回车」,在这个前提下,如何完成需求,你的代码非常好,没有问题。只是少了「当页面加载后」的这个含义。
    ljpCN
        23
    ljpCN  
       2 天前
    @ltaoo1o 如果你是想要在整个页面监听回车按键,先不讨论这个需求的合理性,为了实现你要的语义清晰,你应该寻求对 hooks 的封装来实现你的 log 函数拿到最新的 state ,或者直接通过 ref 获取 input 标签当前的 value 。前者的话举一个例子: https://ahooks.js.org/hooks/use-memoized-fn
    shintendo
        24
    shintendo  
       2 天前
    @ljpCN 你这个跟他的不一样,他监听了 document ,在输入框外面 enter 也能触发
    shintendo
        25
    shintendo  
       2 天前
    @ltaoo1o 容易漏是指漏依赖? eslint 可以检查啊
    ltaoo1o
        26
    ltaoo1o  
       2 天前
    @shintendo #25 心智负担,而且代码会比较丑🤣
    ljpCN
        27
    ljpCN  
       2 天前
    @shintendo 嗯,我上面也说了要想全局监听的解决方案。另外我主要想表达的是 hooks 带来的心智负担不足以成为因噎废食的理由。 @ltaoo1o 这位老哥所说的“用函数组件就是写不出来”是不成立的;“hook 改变了代码作为领域知识的意义,现在都是在写框架代码,不是写业务代码”则缺少论证看起来是个人偏见。
    LOWINC
        28
    LOWINC  
       1 天前
    你这个前提就有问题
    “1. 该代码表示「当页面初始化后」,所以这里不能依赖 log 、handleKeyDown 、”

    可以看下 dan 的文章 https://overreacted.io/a-complete-guide-to-useeffect/

    @ltaoo1o
    ltaoo1o
        29
    ltaoo1o  
       1 天前
    @LOWINC 我理解你的意思,useEffecet(fn, []) 不能表达「当页面初始化后」,和 componentDidMount 不同,这也是我无语的一点,函数组件没有一个明确的函数、方式来表达「当页面、组件初始化后」。
    现在社区普遍都将 useEffecet(fn, []) 作为「当页面初始化」的含义来用不是吗,如果不这么写,可以给我一个方案吗,我确实不懂该如何写
    dango33
        30
    dango33  
       1 天前
    @ltaoo1o #4 https://zh-hans.react.dev/learn/separating-events-from-effects
    建议重新看一遍 React 的教程,把有些重要问题解释得很清楚了。
    ltaoo1o
        31
    ltaoo1o  
       1 天前
    @dango33 完整看完了,这个教程不是证明了我说的吗,后面提到了 useEffectEvent ,一个「还没有发布的实验性 API 」,如果我用 useEffectEvent 包 log 就能解决我的问题,为什么要发布一个新的 hook ,就是有一些场景用 useEffect 解决不了。
    另外,如果希望讨论,可以把你的观点明确地表达出来,我承认自己很菜,也欢迎讨论。
    dango33
        32
    dango33  
       1 天前
    @ltaoo1o #31 “建议你读教程”并不是在尝试讽刺你菜,闻道有先后,不是说只有菜鸟才看教程。如果还没有读过,建议快速过一遍,尤其是对于从 class 时代过来的人。主要是能够(在一定程度上)避免踩坑,不至于对你提出的这个问题感到恼火(因为这并不是现阶段很难发现和解决的问题)。

    如果对#15 的方式不满意,也可以选择把你原来的 log 函数扔给 ref ,这样更新 ref 的 effect 就只有一个依赖了。
    或者用组件外的一个实例记录要上传的数据,提供暂存和上传数据的方法给组件用,这样 useEffect 就完全没有依赖了。(一点拙见)
    ltaoo1o
        33
    ltaoo1o  
       1 天前
    @dango33 #32 我后面也说了,当我发现不对时,我就意识到哪里有问题,应该怎么写。但任何写法,都违背了我希望用代码表达的含义。其次,一个需要专门写文章来说明,并且还要发布新的 API 来支持更多场景的坑,不让人恼火吗,我写业务就够累了还要处处小心这种坑。
    另外,我现在所有代码都是像你说的另外起实例,很少用 hook 了,像这样

    ```js
    function HomePage() {
    const [state, setState] = useState($page.state);
    useEffect(() => {
    $page.onStateChange((v) => setState(v));
    $page.ready();
    }, []);
    return ();
    }
    ```

    这样写就不用考虑框架的坑了,有问题也是我自己的问题了。
    zhengfan2016
        34
    zhengfan2016  
       1 天前
    第一次见到 react.fc 外面使用 settimeout ,黑马程序员也不是这么教的吧,这么写肯定容易出问题。就和不用 const 偏要用 var 一样
    dango33
        35
    dango33  
       1 天前
    @ltaoo1o #33 函数组件整个一大闭包,所以这些“坑”对于我来说反而挺符合直觉的 XD 。
    ooo4
        36
    ooo4  
    OP
       1 天前
    @zhengfan2016 因为我在调试 react 源码,不想通过合成事件去触发,徒增额外调试
    demonzoo
        37
    demonzoo  
       1 天前
    ltaoo1o
        38
    ltaoo1o  
       1 天前
    @demonzoo 哥,感谢你,可是不是我想要的 🤣 我在注释里写了不能依赖 handleKeydown 等,不然语义就变了,变成了「当 handleKeydown 」改变时,监听 keydown 事件。但是我希望表达的是「当页面加载后,监听事件」
    ljpCN
        39
    ljpCN  
       11 小时 24 分钟前
    @ltaoo1o 不知道你是没看到还是无视了我上面发的消息,既然你还是认为没有解决方案,我只能把解决方案写好发出来了: https://codesandbox.io/p/sandbox/zuo-yong-yu-de-wen-ti-forked-rqm5jy
    ljpCN
        40
    ljpCN  
       11 小时 16 分钟前
    @ltaoo1o 另外如果你真的接受了 react 文档中关于 useEffect 的定义,你应该知道 useEffect 的语义并非是 xx 发生变化时执行 yy 。useEffect 只是一个渲染过程的副作用,在严格模式下即使组件只挂载一次它也会执行两次。你不能把 useEffect 当做监听状态变化的回调来使用,而是应当作为每次渲染完成后的副作用来理解,只要你在 useEffect 的返回函数里对副作用做好适当的清理,你是不用去关心它执行了一次还是两次还是每次渲染后都执行的。当然你也可以用 useMemoizedFn 这样的 hook 来减轻你理解函数当前使用的是哪一次渲染的闭包变量的心智负担,这也完全没问题。
    ltaoo1o
        41
    ltaoo1o  
       11 小时 11 分钟前
    @ljpCN #39 抱歉是没看到,你的实现很巧妙,很厉害。
    ltaoo1o
        42
    ltaoo1o  
       11 小时 6 分钟前
    @ljpCN #40 确实是我太菜了,我很多框架、语言都写,无论 vue 、flutter 还是 swift 这些,都是有明确的 组件加载后 的语义的,我也按这个习惯来写,导致对 react 这里不熟悉,抱歉抱歉,忽略了你的消息。
    demonzoo
        43
    demonzoo  
       4 小时 59 分钟前
    @ltaoo1o 哥,感谢你,可是不是我想要的 🤣 我在注释里写了不能依赖 handleKeydown 等,不然语义就变了,变成了「当 handleKeydown 」改变时,监听 keydown 事件。但是我希望表达的是「当页面加载后,监听事件」
    ================
    听哥们一句劝,其实我觉得其他人提到的在 input 里加 onKeyDown handler 是最好的方法,但你既然要用 useEffect 那我就帮你写了一个用 useEffect 实现的方法。
    40 楼的哥们说的对,你对 useEffect 的理解恐怕有点偏颇,而且我觉得你对语义的追求也有些极端,这跟 “茴字的几种写法” 有什么区别呢
    最后,如果你真的采用了 useMemoizedFn 这种解法,那我觉得真的是高射炮打蚊子了,本来几行的代码非要写成一百行,应用了一堆 useRef ,useMemo ,useEffect 等 hook ,本末倒置。。。
    ltaoo1o
        44
    ltaoo1o  
       4 小时 23 分钟前
    @demonzoo #43 是的我承认我对 useEffect 的理解不对,语义这个算个人追求,它能让我在不同框架、语言,用一套思维去写前端,也能让代码有可迁移、可维护、更好理解,当然这也是个人追求,不这样做完全是可以的,尤其是公司项目,能写完就行。
    最后忘了怎么做的,是公司项目,代码比较复杂。我自己项目是抛弃了 hook ,只把 react 当视图渲染用,没有这些问题。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1148 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 17:58 · PVG 01:58 · LAX 09:58 · JFK 12:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.