React effects 的闭包里锁定 state 值是怎么实现的? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
ethusdt
0.01D
V2EX    React

React effects 的闭包里锁定 state 值是怎么实现的?

  •  
  •   ethusdt
    FaiChou 2022-02-23 21:21:43 +08:00 2522 次点击
    这是一个创建于 1326 天前的主题,其中的信息可能已经有所发展或是发生改变。
    function Page() { const [a, setA] = React.useState(0); useEffect(() => { const interval = setInterval(() => { console.log(a) }, 2000) return () => clearInterval(interval) }, []); return ( <div> <span>{a}</span> <button title="update" OnPress={() => setA(Math.random())} /> </div> ); } 

    不管如何点击按钮, 打印的都是初始值 0.

    看过的一些资料, 都说是capture外部的数据.

    但是始终想不明白, 当一个变量找不到时, 会去上一层 scope 里查找, 所以应该是找的到最新数据的, 比如将代码改成:

    function Page() { let a = 0; useEffect(() => { const interval = setInterval(() => { console.log(a) }, 2000) return () => clearInterval(interval) }, []); return ( <div> <button title="update" OnPress={() => a++} /> </div> ); } 

    当捕获的变量不是一个 state 时候, 它就可以打印最新的值.

    我自己写了一个简单实现:

    const hooks = []; function useState(val) { let state = hooks[0] || val; hooks[0] = state; function setVal(v) { state = v; hooks[0] = state; } return [state, setVal]; } let cleanup = null; function useEffect(callback) { if (cleanup) cleanup() cleanup = callback(); } function Foo() { const [count, setCount] = useState(0); useEffect(() => { setTimeout(() => { console.log(count)}, 1000); }) return setCount; } var setCount = Foo(); // log 0 setCount(1); Foo(); // log 1 

    但这肯定不是 react 的实现, 具体实现也没有搞懂, 所以就想问问, 这里用的什么方式处理的?

    9 条回复    2022-02-24 09:23:37 +08:00
    zzuieliyaoli
        1
    zzuieliyaoli  
       2022-02-23 21:26:03 +08:00   1
    dcsuibian
        2
    dcsuibian  
       2022-02-23 21:35:11 +08:00
    说一下我的猜测,仅仅是猜测:
    第一种写法:
    在第 1 次渲染的时候,也就是 Page 函数第一次调用的时候,假设 a 代表的是计算机地址 0x12345678 ,里面装的内容是数字 0 。而在你 setA(Math.random())之后,第二次调用 Page 函数,这时候虽然变量名还是 a ,不过地址变了,例如 0x23456789 ,里面装着内容 1 。而那个箭头函数里的 a 实际上取了 0x12345678 的内容

    第二种写法,假设 a 还是代表地址 0x12345678 ,由于你写的是 a++,那么在你按下按钮的时候,0x12345678 里的内容就变成 2 了,而函数里 a 指的是 0x12345678 ,取出的值自然就变了
    joesonw
        3
    joesonw  
       2022-02-23 21:57:50 +08:00 via iPhone   1
    useEffect(() => {}, [a])

    没有加 dependency 的时候,里面的 a 是第一次调用 useEffect 的时候的 closure

    要理解原理,搜索 react hook fiber 。大致就是 hook 方法是链表串起来的。避免每次 render 都调用没改变的地方。
    Zhuzhuchenyan
        4
    Zhuzhuchenyan  
       2022-02-23 22:37:38 +08:00
    简单翻了一下,粗浅的理解是`<span>{a}</span>`中的{a}看似是一个闭包捕获,但其实本质上是函数调用`React.createElement("span", null, a)`中的一个形参
    于是在后续逻辑中` <span>{a}</span>`中的 a 和`setInterval(() => { console.log(a) }, 2000)`中的 a 基本上没有任何关系,产生上文所说结果就是很自然的了
    ethusdt
        5
    ethusdt  
    OP
       2022-02-23 22:47:25 +08:00
    @zzuieliyaoli Dan 的这篇我看了, 只是说了下这个现象, 并没有讲是怎么实现的.
    ethusdt
        6
    ethusdt  
    OP
       2022-02-23 22:50:51 +08:00
    @dcsuibian 这和 a++ setA(Math.random()) 没关系...
    sweetcola
        7
    sweetcola  
       2022-02-23 23:13:40 +08:00
    我写了个小 Demo 来展示这种差异(变量名请无视...)
    ```Javascript
    var t = (() => {
    let num = 1;
    let cb = undefined;
    let cbUpdated = false;
    return {
    a:()=>([num, (n) => { num = n; }]),
    b:(c) => {
    if (!cbUpdated) {
    cbUpdated = true;
    cb = c;
    }
    cb()
    }
    }
    })();
    var f = () => {
    let [a, setA] = t.a();
    let b = 1
    t.b(() => setInterval(() => {console.log(a, b);}, 1000))
    return {
    tt: () => {
    let newNum = Math.random()
    setA(newNum)
    b = newNum
    }
    };
    }
    var tmp = f()
    ```
    在控制台粘贴以上代码后可以看到输出了"1 1",这个时候输入 tmp.tt() 后会变成 "1 Math.random()"。也就是 state 没有变。是你就算再次执行 f 函数,输出的 state 依然会是 1 ,因为代码中的 cb 并没有被更新。

    这时就需要让 cb 更新来让 t.b 获取新 state ,也就是 useEffect 的 dependencyList 。把上面代码的 b 函数改成:
    ```
    b:(c) => {
    cb = c
    cb()
    }
    ```
    后再次执行 f 函数可以看到成功输出新 state 了。这种特性存在于“闭包中的闭包”。这就是 Hooks 的奥秘,整个 React-Hook 可以理解成一个大闭包。(不知道有没有说错...)
    dablwow
        8
    dablwow  
       2022-02-24 09:17:01 +08:00
    这就是一个最直白的闭包问题。

    两个点:
    一是
    ```<button title="update" OnPress={() => setA(Math.random())} />```

    这里的 setA 会触发 re-render ,因此函数首次执行生成的 a ,始终都是初始值0 ;而定时器读取的都是这个 a ,后续渲染的 a ,这里读不到。


    二是,useEffect 的 dependencies 传了空数组,因此 useEffect 内的函数只有首次渲染会执行。
    尽管 a 的值在后续渲染中的确改变了,但没执行定时器,也就无法打印。

    可以把 dependencies 去掉,变成每次都执行,打印结果就会显示最新的 a 了(尽管还是会混杂旧的 a )
    dablwow
        9
    dablwow  
       2022-02-24 09:23:37 +08:00
    用一个能改变的例子作对比,可以更好地理解:

    首先 a 由常量改为变量:
    ```let [a, setA] = React.useState({ value: 0 });```

    其次设置时不要走 set 函数,直接修改:
    ```<button title="update" OnPress={() => a = Math.random() } />```

    这样定时器就能打印最新的 a 了。为啥?因为这时候 useEffect 生成的闭包中,a 变了。而题目的例子,a 没变,变的是第二 /三 /n 的 a
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2728 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 07:47 PVG 15:47 LAX 00:47 JFK 03:47
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86