Hooks 简化了 React 组件内部状态和副作用的管理。 此外,可以将重复的逻辑提取到自定义 Hooks 中,以在整个应用程序中重复使用。
Hooks 严重依赖于 JS 闭包。这就是为什么 Hooks 如此具有表现力和简单,但是闭包有时很棘手。
使用 Hooks 时可能遇到的一个问题就是过时的闭包,这可能很难解决。
让我们从过时的装饰开始。 然后,看看到过时的闭包如何影响 React Hooks,以及如何解决该问题。
1.过时的闭包
工厂函数 createIncrement(incBy)
返回一个increment
和log
函数的元组。 调用时,increment()
函数将内部value
增加incBy
,而log()
仅打印一条消息,其中包含有关当前value
的信息:
function createIncrement(incBy) {
let value = 0;
function increment() {
value += incBy;
console.log(value);
}
const message = `Current value is ${value}`; function log() { console.log(message); }
return [increment, log];
}
const [increment, log] = createIncrement(1);
increment(); // 1
increment(); // 2
increment(); // 3
// 不能正确工作!
log(); // "Current value is 0"
[increment, log] = createIncrement(1)
返回一个函数元组:一个函数增加内部值,另一个函数记录当前值。
然后,increment()
的3次调用将 value
递增到3。
最后,log()
调用打印消息是 Current value is 0
,这有点出乎意料的,因为此时 value
为 3
了。
log()
是一个过时的闭包。闭包 log()
捕获了值为 "Current value is 0"
的 message
变量。
即使 value
变量在调用increment()
时被增加多次,message
变量也不会更新,并且总是保持一个过时的值 "Current value is 0"
。
2.修复过时的闭包
修复过时的log()
问题需要关闭实际更改的变量:value
的闭包。
我们将语句 const message = ...;
移动到 log()
函数内部:
function createIncrement(incBy) {
let value = 0;
function increment() {
value += incBy;
console.log(value);
}
function log() {
const message = `Current value is ${value}`; console.log(message);
}
return [increment, log];
}
const [increment, log] = createIncrement(1);
increment(); // 1
increment(); // 2
increment(); // 3
// Works!
log(); // "Current value is 3"
现在,在调用了 3 次 increment()
函数之后,调用 log()
记录了实际value
:"Current value is 3"
。
3. Hooks 中的过时闭包
### 3.1 useEffect()
我们来看一下使用useEffect()
过时闭包的常见情况。
在组件<WatchCount>
中,useEffect()
中每2秒记录一次count
的值
function WatchCount() {
const [count, setCount] = useState(0);
useEffect(function() {
setInterval(function log() {
console.log(`Count is: ${count}`);
}, 2000);
}, []);
return (
<div> {count} <button onClick={() => setCount(count + 1) }> Increase </button> </div>
);
}
打开事例(codesandbox.io/s/stale-clo…)
并点击几次增加按钮。然后看看控制台,每2秒出现一次Count is: 0
,尽管count
状态变量实际上已经增加了几次。
为什么会这样?
第一次渲染时,状态变量count
初始化为0
。
组件安装后,useEffect()
调用 setInterval(log, 2000)
计时器函数,该计时器函数计划每2秒调用一次log()
函数。 在这里,闭包log()
捕获到count
变量为0
。
之后,即使在单击Increase
按钮时count
增加,计时器函数每2秒调用一次的log()
,使用count
的值仍然是0
。log()
成为一个过时的闭包。
解决方案是让useEffect()
知道闭包log()
依赖于count
,并在count
改变时正确处理间隔的重置
function WatchCount() {
const [count, setCount] = useState(0);
useEffect(function() {
const id = setInterval(function log() {
console.log(`Count is: ${count}`);
}, 2000);
return function() {
clearInterval(id);
}
}, [count]);
return (
<div>
{count}
<button onClick={() => setCount(count + 1) }>
Increase
</button>
</div>
);
}
正确设置依赖项后,一旦count
发生变化,useEffect()
就会更新闭包。
3.2 useState()
<DelayedCount>
组件有1个button
,以1秒延迟异步增加计数器。
function DelayedCount() {
const [count, setCount] = useState(0);
function handleClickAsync() {
setTimeout(function delay() {
setCount(count + 1);
}, 1000);
}
return (
<div> {count} <button onClick={handleClickAsync}>Increase async</button> </div>
);
}
现在打开演示(codesandbox.io/s/use-state… 快速单击2次按钮。 计数器仅更新为1
,而不是预期的2
。
每次单击setTimeout(delay, 1000)
将在1秒后执行delay()
。delay()
此时捕获到的 count
为 0
。
两个delay()
都将状态更新为相同的值:setCount(count + 1) = setCount(0 + 1) = setCount(1)
。
这是因为第二次单击的delay()
闭包中已捕获了过时的count
变量为0
。
为了解决这个问题,我们使用函数式方法setCount(count => count + 1)
来更新count
状态
function DelayedCount() {
const [count, setCount] = useState(0);
function handleClickAsync() {
setTimeout(function delay() {
setCount(count => count + 1); }, 1000);
}
function handleClickSync() {
setCount(count + 1);
}
return (
<div>
{count}
<button onClick={handleClickAsync}>Increase async</button>
<button onClick={handleClickSync}>Increase sync</button>
</div>
);
}
打开演示(codesandbox.io/s/use-state… 再次快速单击按钮2
次。 计数器显示正确的值2
。
当一个返回基于前一个状态的新状态的回调函数被提供给状态更新函数时,React确保将最新的状态值作为该回调函数的参数提供
setCount(alwaysActualStateValue => newStateValue);
这就是为什么在状态更新过程中出现的过时装饰问题可以通过函数这种方式来解决。
4.总结
当闭包捕获过时的变量时,就会发生过时的闭包问题。
解决过时闭包的有效方法是正确设置React钩子的依赖项。或者,在失效状态的情况下,使用函数方式更新状态。
~完,我是小智,我要去刷碗了。
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。
原文:https://dmitripavlutin.com/react-hooks-stale-closures/
交流
文章每周持续更新,可以微信搜索「 大迁世界 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub github.com/qq449245884… 已经收录,整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,另外关注公众号,后台回复福利,即可看到福利,你懂的。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!