You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
functioncreateIncrement(i){letvalue=0;functionincrement(){value+=i;console.log(value);constmessage=`Current value is ${value}`;returnfunctionlogValue(){console.log(message);};}returnincrement;}constinc=createIncrement(1);constlog=inc();// 1inc();// 2inc();// 3log();// "Current value is 1"
functioncreateIncrementFixed(i){letvalue=0;functionincrement(){value+=i;console.log(value);returnfunctionlogValue(){constmessage=`Current value is ${value}`;console.log(message);};}returnincrement;}constinc=createIncrementFixed(1);constlog=inc();// 1inc();// 2inc();// 3log();// "Current value is 3"
functioncreateIncrementFixed(i){letmessage;letvalue=0;functionincrement(){value+=i;console.log(value);message=`Current value is ${value}`;returnfunctionlogValue(){console.log(message);};}returnincrement;}constinc=createIncrementFixed(1);constlog=inc();// 1inc();// 2inc();// 3log();// "Current value is 3"
functionApp(){const[count,setCount]=useState(0);constcountRef=useRef(0)countRef.current=countfunctionhandleAlertClick(){setTimeout(()=>{alert("You clicked on: "+countRef.current);},3000);}return(<div><p>You clicked {count} times</p><buttononClick={()=>setCount(count+1)}>Click me</button><buttononClick={handleAlertClick}>Show alert</button></div>);}
这时候重复上述过程, 可以发现 3 秒之后能得到当前的最新值.
关于 useEffect
实际上 useEffect 也是一个函数, 和 handleAlertClick类似, 也可以实现类似需求: 即在组件 mount 的时候根据初始 state 只发送一个异步请求, 用户在等待请求的过程中对该 state 重新进行了设置 那么该请求中所涉及到的 state 应该是在 mount 时候的初始值, 也就是 initial state, 代码如下:
functionApp(){const[count,setCount]=useState(0);useEffect(()=>{setTimeout(()=>{alert("You clicked on: "+count);},3000);},[])return(<div><p>You clicked {count} times</p><buttononClick={()=>setCount(count+1)}>Click me</button></div>);}
在页面初次渲染之后便开始发送请求, 此时点击几次Click me按钮, 3 秒之后会显示You clicked on: 0
当然, 如果想要实现在 mount 时发送请求携带的 state 是最新的用户操作过后的数据, 那么还是一样可以使用useRef()来存取最新的 state, 代码改成如下:
functionApp(){const[count,setCount]=useState(0);constcountRef=useRef(0);countRef.current=count;useEffect(()=>{setTimeout(()=>{alert("You clicked on: "+countRef.current);},3000);},[])return(<div><p>You clicked {count} times</p><buttononClick={()=>setCount(count+1)}>Click me</button></div>);}
这样再点击Clik me之后, state 发生了修改, 那么之前发送的请求中的 state 也就是修改过后的最新的 state.
简单聊一聊 hooks 与闭包
变量引用
关于这方面问题不做深究, 可以看做是指针
闭包
这里重点谈谈第二点, 关于闭包里的变量引用问题.
例子 1:
分析:
outer()
函数创建了一个闭包, 闭包中的变量为x = 0
,inner
函数和change
在被创建的过程中形成词法作用域, 可以访问到该变量inner()
函数, 此时在当前的词法作用域下创建一个新的闭包, 该闭包中创建了一个新的变量_x
, 当然还存在之前引用的x
变量.log
函数在被创建时形成词法作用域, 可以访问到_x
和x
, 当然这两个变量目前相等change()
函数, 该函数在第一次声明的词法作用域下修改了x
变量, 使其为1
log()
函数, 注意, 由于闭包的特性,log()
函数可以访问到x
和_x
, 但由于change()
修改了最顶层的词法作用域里的x
, 这里读取的x
也为1
. 不过, 由于这里的_x
只在inner()
函数调用(也就是声明log
函数)的时候声明一次, 且指向x = 1
, 即使change()
修改了x
对其并无任何影响. 因此这时的log()
函数调用的词法作用域为x = 1, _x = 0
例子 2:
该例子取自于 react-hooks-stale-closures 这篇文章. 虽然我个人觉得这篇文章的作者其实没写到点子上...
有如下两段代码
这里就不细展开了, 具体的分析可以看 这个问题下的答案, 主要需要注意这几点:
value
在最顶层的词法作用域, 确实是不断在变化的log()
函数在被调用的时候, 确实拿到的value
值是最新的值, 但是第一段代码与第二段代码的区别在于message
变量, 第一段代码中的message
在value
修改值之后其保存的value
仍旧是原始的值(也就是 1)(此 value 非彼 value), 而第二段代的message
并不是作为log
函数的定义时的闭包变量而存在, 而是作为自己的作用域内的变量. 因此在调用log()
函数的时候, 才会声明message
, 这时候再去查找value
, 得到的当然是最新值对于第二段代码, 这么改效果也是一样的:
分析: 每次调用
inc()
的时候不仅修改顶层词法作用域里的value
, 也在不断重写message
, 使其内部的value
和value
变量始终保持一致hooks
从计时器开始
上一篇已经讲到, 可以使用闭包来模拟类的行为, 还是以计数器为例, 假定有如下代码:
可以看到结果, 2 秒以后输出值全为 3. 然而往往需求可能是, 每次打印出来的值应该是顺序的, 比如在这个例子里面希望 2 秒后依次打印 1, 2, 3
可以这么做:
这里我们使用
v
这个临时变量存取当前(上一次)的value
的值, 这样使得setTimeout
这个闭包内部可以在 2 秒后, "正确"读取到当前属于自己的值. 而对于第一段代码, 由于inc()
不断调用修改了顶层词法作用域里的value
变量,setTimeout
读取到的value
的值永远都是最新的(因为value
不断被更新...)react
引子
先来看一个场景:
Dan 在他的 博客 里面有这样一个场景实例: 有一个类似 twitter 的页面, 你想要 follow 某个用户, 但是点击 follow 这个动作是一个异步请求可能需要时间, 在点击了 follow 这个操作之后, 立马切换到另一个用户的页面, 几秒钟之后客户端收到响应, 显示你 follow 了"这个"用户
具体的 live demo 点击这里查看
然而, 使用函数式组件写法, 和 class 组件写法带来的效果是很不同的, 两种写法的组件和效果分别如下:
class 组件:
函数式组件:
可以看到, 使用函数式组件可以得到正确的 follow, 延时请求所请求的 user 是之前点击的 Follow 按钮的 user, 而非像 class 组件一样发送了错误的 user 请求
下面会进行分析, 当然也推荐直接看 Dan 的 博客
分析
Dan 在另外一篇 文章 中总结的相当好:
render
的时候都形成了一次快照, 保存了所有下面的东西, 每一份快照都是不同且独立的. 即useEffect()
this.state
使其指向永远最新状态例子
有如下代码: live demo
一开始先点击
Show alert
按钮, 然后立马点击 3 次Click me
按钮, 3 秒过后浏览器打印出来的结果为打印出"You clicked on: 0"
更多次实验以后会发现,
Show alert
只会显示在点击触发前那一刻所对应的count
的值, 比如目前count
为 5, 点击Show alert
之后立马再点击几次Click me
, 3 秒过后浏览器打印的结果为 5这是由于闭包的原因, 每次
setTimeout()
读取到的count
的是当前render
状态下的值, 即使后面对count
进行了改变,setTimeout()
中的count
不受影响, 永远是当前 render 下的count
的值, 而非最新的count
的值那如果想要得到最新的值呢?
最简单, 可以使用
useRef()
来存取最新的值, 那么代码可以改成如下:这时候重复上述过程, 可以发现 3 秒之后能得到当前的最新值.
关于 useEffect
实际上
useEffect
也是一个函数, 和handleAlertClick
类似, 也可以实现类似需求: 即在组件 mount 的时候根据初始 state 只发送一个异步请求, 用户在等待请求的过程中对该 state 重新进行了设置 那么该请求中所涉及到的 state 应该是在 mount 时候的初始值, 也就是initial state
, 代码如下:在页面初次渲染之后便开始发送请求, 此时点击几次
Click me
按钮, 3 秒之后会显示You clicked on: 0
当然, 如果想要实现在 mount 时发送请求携带的 state 是最新的用户操作过后的数据, 那么还是一样可以使用
useRef()
来存取最新的 state, 代码改成如下:这样再点击
Clik me
之后, state 发生了修改, 那么之前发送的请求中的 state 也就是修改过后的最新的 state.这里需要注意, 第一段代码中
useEffect()
虽然为空, 但是 eslint 会提示需要加上count
, 但是加上了并不是我们想要的效果, 代码会变成这样:那么最后的结果为, 每次点击
Click me
, 触发count
的更新, 随之也触发useEffect()
这个函数的调用, 那么请求也会每次被发送, 当然由于闭包, 3 秒之后会依次显示所对应的 count 的值, 比如点击了 3 次, 那么 3 秒过后依次打印 1, 2, 3总结: 需要分析清楚你想要的是什么, 是需要最新的
state/props
(可以用ref
), 还是想要每次 render 下所对应的自己的state/props
另外, 非常推荐 Dan 的 A Complete Guide to useEffect, 虽然很长, 但是讲的是非常细致, 当然啃下来还是不容易的...
参考
The text was updated successfully, but these errors were encountered: