使用React Hooks时要注意陈旧的Closures及修复教程

360 阅读3分钟

钩子缓解了React功能组件内部的状态和副作用的管理。此外,重复的逻辑可以被提取到一个自定义的钩子中,以便在整个应用程序中重复使用。

钩子在很大程度上依赖于JavaScript闭包。这就是为什么钩子是如此具有表现力和简单。但闭包有时是很棘手的。

在使用钩子时,你可能会遇到的一个问题是陈旧的闭包。而这可能是很难解决的!

让我们先来提炼一下什么是陈旧的封闭。然后你会看到陈旧的闭包如何影响React钩子,以及如何解决这个问题。

1.陈旧的闭包

一个工厂函数createIncrement(incBy) 返回一个由incrementlog 函数组成的元组。当被调用时,increment() 函数将内部的value 增加到incBy ,而log() 只是简单地记录了一条信息,其中包含了当前的value

javascript

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(); // logs 1
increment(); // logs 2
increment(); // logs 3
// Does not work!
log();       // logs "Current value is 0"

[increment, log] = createIncrement(1) 返回一个函数的元组:一个函数增加内部值,另一个函数记录当前值。

然后,对increment() 的3次调用将value 递增到3

最后,对log() 的调用记录了信息"Current value is 0" 。嗯......这出乎意料,因为value 等于3

log() 是一个陈旧的闭包。闭包 已经捕获了 变量,有 。log() message "Current value is 0"

即使value 变量在调用increment() 时被多次递增,message 变量也不会更新,总是保持一个过时的值"Current value is 0"

陈旧的闭包捕获了具有过期值的变量。

让我们看看如何修复陈旧的闭包的一些方法。

2.修复陈旧的闭包

修复陈旧的log() ,需要在实际改变的变量上关闭闭包:value

让我们把语句const message = ...; 移到log() 函数体内。

javascript

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(); // logs 1
increment(); // logs 2
increment(); // logs 3
// Works!
log();       // logs "Current value is 3"

现在,在调用了3次increment() 函数后,调用log() 记录了实际的value:"Current value is 3"

log() 不再是一个陈旧的封闭。

3.钩子的陈旧闭包

3.1useEffect()

让我们研究一下使用useEffect() 钩子时的一个常见的陈旧闭合案例。

在组件<WatchCount> ,钩子useEffect() ,每2秒记录一次count 的值。

jsx

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>
  );
}

打开演示,点击几次增加按钮。然后看控制台,每2秒出现一次Count is: 0 ,尽管事实上count 状态变量实际上已经被增加了几次。

React Stale Closure

为什么会出现这种情况?

在第一次渲染时,状态变量count 被初始化为0

在组件安装完毕后,useEffect() 调用setInterval(log, 2000) 定时器函数,该函数安排每2秒调用一次log() 函数。在这里,闭包log() 捕获count 变量为0

后来,即使count 在点击增加按钮时增加,每2秒由定时器函数调用的log() 闭包仍然使用count 作为初始渲染时的0log() 成为一个陈旧的闭包。

解决办法是让useEffect() ,让闭包log() 依赖于count ,并在count 改变时正确处理间隔的重置。

jsx

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() 就会更新闭包。

打开固定的演示,点击几次增加。控制台会记录下count 的实际值。

React Stale Closure Fixed

正确管理钩子的依赖关系是解决陈旧闭合问题的有效方法。

我建议启用eslint-plugin-react-hooks,它可以检测被遗忘的依赖关系。

3.2useState()

该组件<DelayedCount> 有1个按钮Increase async,以1秒的延迟异步增加计数器。

jsx

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>
  );
}

现在打开这个演示。快速点击2次增加异步按钮。计数器只被1 更新,而不是预期的2

每次点击setTimeout(delay, 1000) ,都会在1秒后安排执行delay()delay() 捕获变量count ,作为0

两个delay() 闭包(因为已经进行了2次点击)将状态更新为相同的值:setCount(count + 1) = setCount(0 + 1) = setCount(1)

所有这些都是因为第二次点击的delay() 闭包已经捕获了过时的count 变量为0

为了解决这个问题,让我们用一种功能性的方式setCount(count => count + 1) 来更新count 状态。

jsx

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>
  );
}

现在setCount(count => count + 1) 更新了delay() 里面的计数状态。

打开演示。快速点击增加异步2次。counter 显示的是正确的值2

当一个基于前一个状态返回新状态的回调函数被提供给状态更新函数时,React会确保最新的状态值被作为参数提供给该回调。

javascript

setCount(alwaysActualStateValue => newStateValue);

这就是为什么在状态更新过程中出现的陈旧闭合问题通常会通过使用功能化的方式来更新状态而得到很好的解决。

4.结论

当一个闭包捕捉到过时的变量时,就会出现陈旧的闭包问题。

解决陈旧闭包的有效方法是正确设置React钩子的依赖关系。或者,在状态过期的情况下,使用功能性的方式来更新状态。