React Hooks 与闭包

0 阅读4分钟

一、闭包(Closure)

1. 什么是闭包

闭包 = 自由变量的集合,在外部函数销毁后,内部函数仍能访问这些变量。

核心原理:词法作用域规则与"函数执行完毕上下文会被销毁"的规则冲突,JS 引擎将内部函数需要的变量放到堆内存中持久化保存。

2. 代码示例解析

示例1:基础闭包(closure/1.js)

function foo() {
  function bar() {
    var a = 1
    console.log(b);  // 引用外部变量 b
  }
  var b = 2;
  return bar
}

const baz = foo()  // 返回 bar,foo 执行完毕但 b 被保留
baz();  // 输出 2

关键点bar 被返回后,foo 虽然执行完毕,但变量 b 不会被销毁,因为 bar 通过闭包引用了它。


示例2:函数记忆/计数器(closure/2.js)

function Add(){
    let num = 0;
    return function(){
        num++; 
        return num;
    }
}
const res = Add()  // 创建独立作用域,num=0 被保存
console.log(res());  // 1
console.log(res());  // 2

关键点:闭包可以保存状态,实现函数记忆。每次调用 res() 都会基于上一次的 num 值累加。


示例3:词法作用域(closure/3.js)

function foo(){
    var myName = '极客邦'
    bar()
}
function bar(){
    console.log(myName);  // 输出什么?
}
var myName = '极客时间'
foo()

关键点:输出 极客时间。词法作用域是静态的,由函数声明位置决定。bar 声明在全局作用域,所以它查找 myName 时找到的是全局变量,而不是 foo 内部的。


示例4:闭包封装(closure/4.js)

function foo(){
    var myName = '极客时间'
    let test1 = 1;
    const test2 = 2;

    var innerBar = {
        getName:function(){
            console.log(test1);  // 1
            return myName
        },
        setName:function(newName){
            myName = newName  // 通过闭包修改外部变量
        }
    }
    return innerBar
}

let bar = foo()   
bar.setName('极客邦')
console.log(bar.getName());  // 极客邦

关键点:闭包可以实现数据封装,通过 setName 修改内部变量,通过 getName 获取。

3. 闭包的垃圾回收

场景回收策略
闭包引用函数是全局变量一直存在直到页面关闭,可能内存泄漏
闭包引用函数是局部变量函数销毁后,JS 引擎判断回收

4. 作用域链与词法作用域

作用域链:每个执行上下文的变量环境中包含一个外部引用(outer),指向外部执行上下文。变量查找时沿着当前上下文 → outer → outer → ... → 全局的路径查找。

词法作用域:作用域由代码中函数声明的位置决定,是静态的。词法作用域在代码编译阶段就决定好了,和函数如何调用无关。


二、React Hooks

1. useState 惰性初始化

function heavyCalc() {
  console.log('🔥 执行昂贵计算');
  let sum = 0;
  for(let i = 0; i < 1000000; i++) sum += i;
  return sum;
}

export default function App() {
  // ✅ 惰性初始化:传入函数,仅挂载时运行一次
  const [value, setValue] = useState(() => heavyCalc());

  // ❌ 普通写法:每次重渲染都会调用 heavyCalc()
  // const [value, setValue] = useState(heavyCalc());

  return (
    <div>
      <p>结果:{value}</p>
      <button onClick={() => setValue(v => v + 1)}>点我更新state</button>
    </div>
  );
}

核心要点

  • 传入函数 → 惰性初始化,只在首次挂载时执行
  • 传入 → 每次渲染都会执行计算,浪费性能

2. useState 闭包陷阱

const App = () =>{
  const [count, setCount] = useState(0);
  
  useEffect(() =>{
    const timer = setInterval(()=>{
      // ❌ 闭包陷阱:count 永远是 0
      // setCount(count + 1)
      
      // ✅ 函数式更新:拿到最新值
      setCount(pervCount => pervCount + 1)
    }, 1000) 
    return () => clearInterval(timer)
  }, [])  // 空依赖数组
  
  return <div>{count}</div>
}

闭包陷阱详解

  • useEffect 的依赖数组为空 [],所以回调只在挂载时执行一次
  • setInterval 的回调函数捕获了当时的 count = 0
  • 如果用 setCount(count + 1),每次都是 0 + 1 = 1,计数器失效
  • 解决方案:使用函数式更新 setCount(prev => prev + 1),让 React 传入最新值

3. useState 批量更新

const handleClick = () =>{
  // ❌ 结果是 1,不是 3
  // 批量更新会异步合并
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  
  // ✅ 函数式更新,结果是 3
  setCount((prevCount) => prevCount + 1)
}

核心要点

  • React 会批量合并多次 state 更新,减少渲染次数
  • 直接传值会基于当前渲染周期的旧值
  • 函数式更新确保基于最新值计算

三、闭包与 Hooks 的关系

React 函数组件每次渲染都是一次函数调用,Hooks 利用闭包来保存状态:

组件渲染 → 函数执行 → Hooks 通过闭包捕获当前状态
        ↓
    状态更新 → 重新渲染 → 新的闭包捕获新状态
概念说明
闭包函数能访问其声明时的词法作用域变量
Hooks 闭包陷阱回调捕获了旧渲染周期的状态快照,导致拿到过期值
解决方案函数式更新 setState(prev => ...)

这就是为什么 React Hooks 经常和闭包一起讨论——理解闭包是理解 Hooks 行为的基础。