Closures and React

JavaScript闭包概念
React闭包应用
useEffectEvent

Understanding Closures in JavaScript

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives a function access to its outer scope. In JavaScript, closures are created every time a function is created, at function creation time.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

简单来说,闭包就是指一个函数能够“记住”并访问它的词法作用域(也叫环境或上下文),即使这个函数是在它原始定义的作用域之外执行。

核心特性:

  • 闭包可以保存外部函数作用域中的变量,即使外部函数已经返回。
  • 这些变量不会被销毁,因为闭包持有了对它们的引用。
1
2
3
4
5
6
7
8
9
10
function outer() {
const a = 10; // 外层作用域变量
function inner() {
console.log(a); // 内层函数可以访问 a
}
return inner;
}

const innerFunc = outer(); // outer 执行并返回 inner
innerFunc(); // 输出 10

闭包的底层实现

作用域链(Scope Chain):

  • 每个函数在创建时,会附带它的词法作用域(外部函数的变量环境)。
  • 闭包通过作用域链,逐层查找变量。

变量存储和垃圾回收:

  • 局部变量通常存储在栈中,当函数执行完毕会被销毁。
  • 如果闭包引用了这些变量,它们会被移到堆内存中,直到闭包被销毁才会被回收。

Applying Closures in React

React渲染机制与闭包

每次渲染创建一个闭包。

每次渲染的 snapshot 是独立的,与其他渲染互不干扰。

React 的函数式组件在每次渲染时:

  1. 会执行整个函数组件。
  2. 捕获当前渲染时的 state 和 props。
  3. 对于函数组件内部定义的所有函数(包括 event handler 和 useEffect 中的回调),这些函数都会捕获当前渲染时的 state 和 props

useEffect的执行与闭包

每次执行 useEffect 时:

  1. React 会捕获当前渲染时的 state 和 props。
  2. 如果 useEffect 的依赖数组中有变化,React 会重新执行这个 useEffect。
  3. useEffect 内部定义的函数会捕获当前渲染时的 state 值

重新理解React的一些语法/规则

闭包陷阱 Closure Trap

在 JavaScript 中使用闭包时,由于闭包捕获了当前作用域中的变量或状态的“快照”,而不是随时访问变量的最新值,导致程序行为与预期不符的一种情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
console.log(count); // 始终输出初始值 0
}, 1000);

return () => clearInterval(interval);
}, []); // 空依赖数组

return <button onClick={() => setCount(count + 1)}>Increment</button>;
}

useState: set functions

setState((prevState) => prevState + 1)

https://react.dev/reference/react/useState#setstate

函数式更新确保状态更新逻辑基于最新的状态,而不是捕获的旧值。

1
2
3
4
5
const handleClick = () => {
setTimeout(() => {
setCount((prevCount) => prevCount + 1); // 使用最新的 prevCount
}, 1000);
};

useEffect: Dependency Array

https://react.dev/reference/react/useEffect#specifying-reactive-dependencies

useEffect 的依赖数组中明确列出所有依赖项,确保闭包始终访问最新的状态或变量。

1
2
3
4
5
6
7
useEffect(() => {
const interval = setInterval(() => {
console.log(count); // 始终访问最新的 count
}, 1000);

return () => clearInterval(interval);
}, [count]); // 在依赖数组中加入 count

useEffectEvent

https://react.dev/reference/react/experimental_useEffectEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Counter() {
const [count, setCount] = useState(0);

const logCount = useEffectEvent(() => {
console.log(count); // 始终访问最新的 count
});

useEffect(() => {
const interval = setInterval(() => {
logCount(); // 自动访问最新状态
}, 1000);

return () => clearInterval(interval);
}, []); // 不需要手动管理依赖
}

useEffect 中定义普通函数和使用 useEffectEvent 的区别?

普通函数 + useEffect

  • 函数引用不稳定,每次渲染都会重新创建
  • 可能访问到旧的 state 和 props(需要显式管理依赖数组,否则就可能因为陷入其他闭包取到旧的值)

useEffectEvent

  • 自动获取最新状态,无需手动管理依赖

useEffectEvent 代替案

  • useRef to maintain stable references that persist across renders
  • useCallback to memoize functions, ensuring they have stable references