Hooks 简化了 React 组件内部状态和副作用的管理。此外,可以将重复的逻辑提取到自定义 Hooks 中,以在整个应用程序中重复使用。Hooks 严重依赖于 JS 闭包。这就是为什么 Hooks 如此具有表现力和简单,但是闭包有时很棘手。 u& s" P2 D! T! S9 \
使用 Hooks 时可能遇到的一个问题就是过时的闭包,这可能很难解决。让我们从过时的装饰开始。然后,看看到过时的闭包如何影响 React Hooks,以及如何解决该问题。$ J. a' Z6 w, K' e. B
1.过时的闭包1 @$ | O$ F! P9 o- K
工厂函数 createIncrement(incBy) 返回一个increment和log函数的元组。调用时,increment()函数将内部value增加incBy,而log()仅打印一条消息,其中包含有关当前value的信息:
: d$ w j# u1 F# Y& ]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 了。
7 b4 t0 c2 w- Y) f) d log()是一个过时的闭包。闭包 log()捕获了值为 "Current value is 0"的 message 变量。即使 value 变量在调用increment()时被增加多次,message变量也不会更新,并且总是保持一个过时的值 "Current value is 0"。过时的闭包捕获具有过时值的变量。: F }: ]# K1 l+ j
2.修复过时的闭包
7 Z4 c* E0 y) ?) \# L1 y 修复过时的log()问题需要关闭实际更改的变量:value的闭包。我们将语句 const message = ...; 移动到 log() 函数内部:
2 ~7 Y2 J- R0 n( |1 y* J4 z( @4 [3 Ufunction 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"。( W( g7 ]+ n; M v
3. Hooks 中的过时闭包5 v: L4 a/ D2 s1 r& E
3.1 useEffect()
! m$ k6 M! P1 P0 y. j6 _ 我们来看一下使用useEffect() 过时闭包的常见情况。在组件<WatchCount>中,useEffect() 中每2秒记录一次count的值:4 o" n1 y) J7 F. Q1 `
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>
);
} 打开事例(https://codesandbox.io/s/stale-closure-use-effect-broken-2-gyhzk)并点击几次增加按钮。然后看看控制台,每2秒出现一次Count is: 0,尽管count状态变量实际上已经增加了几次。
9 \0 {5 N8 e, A/ k9 V3 h7 @
- M; {9 k/ k' Z4 T* p/ }. C 为什么会这样?第一次渲染时,状态变量count初始化为0。组件安装后,useEffect()调用 setInterval(log, 2000)计时器函数,该计时器函数计划每2秒调用一次log()函数。在这里,闭包log()捕获到count变量为0。7 o5 |& x$ ~1 |: q# c% N) [
之后,即使在单击Increase按钮时count增加,计时器函数每2秒调用一次的log(),使用count的值仍然是0。log()成为一个过时的闭包。
2 r. D" q0 F! [4 T: y$ p; ] 解决方案是让useEffect()知道闭包log()依赖于count,并在count改变时正确处理间隔的重置:
& e5 }% l3 B/ H2 Bfunction 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()就会更新闭包。
1 _2 H. z+ P: R: k0 l3.2 useState()
+ E0 T* ^( V4 U+ [4 q) |& H <DelayedCount>组件有1个button ,以1秒延迟异步增加计数器:; J' s" j1 o% V' v! [
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>
);
} 现在打开演示(https://codesandbox.io/s/use-state-broken-0q994)。快速单击2次按钮。计数器仅更新为1,而不是预期的2。' o+ T! P% D1 D$ b* P! ~
每次单击setTimeout(delay, 1000)将在1秒后执行delay()。delay()此时捕获到的 count 为 0。两个delay()都将状态更新为相同的值:setCount(count + 1) = setCount(0 + 1) = setCount(1)。% V+ l% Z* \6 O3 a# x) S7 F
这是因为第二次单击的delay()闭包中已捕获了过时的count变量为0。为了解决这个问题,我们使用函数式方法setCount(count => count + 1)来更新count状态:9 o8 E, K: W6 y8 M# o
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>
);
} 打开演示(https://codesandbox.io/s/use-state-fixed-zz78r)。再次快速单击按钮2次。计数器显示正确的值2。; h% C$ v+ w! ] ?. O
当一个返回基于前一个状态的新状态的回调函数被提供给状态更新函数时,React确保将最新的状态值作为该回调函数的参数提供:
! }! V& P3 R* p0 K2 s" AsetCount(alwaysActualStateValue => newStateValue); 这就是为什么在状态更新过程中出现的过时装饰问题可以通过函数这种方式来解决。
% W5 G* r& P& q9 U4.总结" a( V) Y# w, j' u" W0 p
当闭包捕获过时的变量时,就会发生过时的闭包问题。解决过时闭包的有效方法是正确设置React钩子的依赖项。或者,在失效状态的情况下,使用函数方式更新状态。
; O& l( k# E" E1 X: _1 b. W: p5 x
|