提问人:sam liu 提问时间:8/25/2023 更新时间:8/25/2023 访问量:73
为什么第一个代码片段可以解决过时的闭包,而第二个代码片段不能?
Why the first code snippet can solve the stale closure and the second one can't?
问:
感觉第一个和第二个没有什么区别。但是第一个代码片段可以解决过时的闭包。那么,为什么第二个不能呢?我真的想不通。谁能从JavaScript闭包的原理来解释它?
// ========================== First code snippet ========================
let _formVal
export default function App() {
const [formVal, setFormVal] = useState('');
_formVal = formVal
const handleSubmit = useCallback(() => {
console.log('_formVal:', _formVal);
}, []);
return (
<>
<input
onChange={(e) => {
setFormVal(e.target.value);
}}
value={formVal}
/>
<MemoziedSuperHeavyComponnent onSubmit={handleSubmit} />
</>
);
}
// ========================== Second code snippet ========================
export default function App() {
const [formVal, setFormVal] = useState('');
const _formVal = formVal
const handleSubmit = useCallback(() => {
console.log('_formVal:', _formVal);
}, []);
return (
<>
<input
onChange={(e) => {
setFormVal(e.target.value);
}}
value={formVal}
/>
<MemoziedSuperHeavyComponnent onSubmit={handleSubmit} />
</>
);
}
答:
在第一个示例中,只创建了一个变量,该变量位于顶部模块范围级别,因此该变量是函数组件读取和更新的变量。_formVal
在第二个示例中,为组件的每次渲染创建多个变量,因为每次重新渲染都会再次调用函数,从而在组件中重新创建变量,包括新作用域上下文中的变量。由于返回的函数引用永远不会改变,并且始终是在初始渲染上创建的第一个函数(由于空依赖关系),该函数将始终访问在其周围作用域中创建的第一个变量,而不是来自在其他作用域中创建的未来渲染的后续变量。_formVal
App
_formVal
useCallback()
[]
_formVal
纯 JavaScript 的小例子:
let memoizedHandleSubmit;
function foo(x) {
let handleSubmit = function() {
return x++;
};
memoizedHandleSubmit ??= handleSubmit; // if `memoizedHandleSubmit` doesn't have a value assigned to it yet, assign the value, otherwise leave it
return memoizedHandleSubmit;
}
const bar = foo(1);
console.log(bar()); // 1
const bar2 = foo(10);
console.log(bar2()); // 2, not 10
这里我们多次调用函数,每次调用它,都会创建一个新变量,然后创建函数。该函数实质上将保存通过第一次调用在函数中创建的第一个函数,后续调用将重用之前创建的函数(这就是您正在做的事情)。创建第一个函数时,它保存的闭包属于它所定义的范围,这意味着该函数可以访问在其外部定义的变量,例如 .当再次调用该函数时,将创建一个具有其自身值的新作用域,并创建一个新函数,但该作用域将被丢弃且未使用,而是返回旧函数。旧函数仍然只知道它最初定义的范围,因为这是它“保存”的闭包,因此它只知道原始调用的值,因此日志打印而不是 .foo
x
handleSubmit
memoizeHandleSubmit
handleSubmit
foo
foo
foo
handleSubmit
useCallback
handleSubmit
handleSubmit
x
foo
x
handleSubmit
handleSubmit
handleSubmit
x
2
10
请注意,有多种方法可以解决过时的状态问题,对于这种特殊情况,您很可能希望用作依赖项,而不是 ,这将创建对更改时的新引用,从而允许您访问其中更新的状态值。[formVal]
useCallback()
[]
handleSubmit
formVal
在您提供的两个代码片段中,乍一看它们看起来非常相似,但它们确实有细微的区别,这与闭包在 JavaScript 中的工作方式有关。让我们来分析一下差异:
第一个代码片段:
let _formVal;
export default function App() {
const [formVal, setFormVal] = useState('');
_formVal = formVal;
const handleSubmit = useCallback(() => {
console.log('_formVal:', _formVal);
}, []);
return (
<>
<input
onChange={(e) => {
setFormVal(e.target.value);
}}
value={formVal}
/>
<MemoizedSuperHeavyComponent onSubmit={handleSubmit} />
</>
);
}
第二个代码片段:
export default function App() {
const [formVal, setFormVal] = useState('');
const _formVal = formVal;
const handleSubmit = useCallback(() => {
console.log('_formVal:', _formVal);
}, []);
return (
<>
<input
onChange={(e) => {
setFormVal(e.target.value);
}}
value={formVal}
/>
<MemoizedSuperHeavyComponent onSubmit={handleSubmit} />
</>
);
}
现在,让我们关注关键区别:
第一个代码片段(带 let _formVal
):
在此代码片段中,在组件外部声明。它在模块顶部声明为 use,使其成为具有模块级作用域的变量。这意味着可以从模块内的任何位置(包括组件内部)访问和修改它。执行此操作时,您将在模块的作用域中将组件状态的值分配给。这将创建对同一变量的引用。_formVal
App
let
App
_formVal = formVal
formVal
_formVal
第二个代码片段(带常量_formVal
):
在此代码段中,在组件内部使用 .这意味着它是一个具有块级作用域的局部变量,特定于组件的功能。它不能在组件外部访问。_formVal
App
const
App
现在,让我们讨论一下这些差异在闭包方面的影响:
在这两种情况下,当函数创建时,它使用 ,它从其周围的作用域中捕获变量以形成闭包。在第一个代码片段中,是一个模块级变量,因此可以在 创建的闭包中访问它。在第二个代码片段中,是一个局部变量,因此也可以在 创建的闭包中访问它。handleSubmit
useCallback
_formVal
useCallback
_formVal
useCallback
在解决“过时的闭包”方面,两个代码片段的行为应该相似,因为在这两种情况下都是通过引用捕获的。执行函数时,它将始终记录 的当前值。_formVal
handleSubmit
_formVal
因此,这两个代码段的工作方式相似并可以解决“过时的闭包”问题的原因是,在这两种情况下都被捕获为参考,从而确保在调用函数时使用正确且最新的值。变量作用域(模块级与块级)的差异不会影响此上下文中的行为。_formVal
handleSubmit
评论