Javascript

React Hooks 中的闭包问题

本文主要是介绍React Hooks 中的闭包问题,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前言

今天中午在领完盒饭,吃饭的时候,正吃着深海鳕鱼片,蘸上番茄酱,那美味,简直无以言表。突然产品急匆匆的跑过来说:“今天需求能上线吧?”我忽然虎躯一震,想到自己遇到个问题迟迟找不到原因,怯怯的回答道:“能...能吧...”,产品听到‘能’这个字便哼着小曲扬长而去,留下我独自一人,面对着已经变味的深海鳕鱼片...一遍又一遍的想着问题该如何解决...

一、从JS中的闭包说起

JS的闭包本质上源自两点,词法作用域和函数当前值传递。

闭包的形成很简单,就是在函数执行完毕后,返回函数,或者将函数得以保留下来,即形成闭包。

关于词法作用域相关的知识点,可以查阅《你不知道的JavaScript》找到答案。

React Hooks中的闭包和我们在JS中见到的闭包并无不同。

定义一个工厂函数createIncrement(i),返回一个increment函数。每次调用increment函数时,内部计数器的值都会增加i

function createIncrement(i) {
    let value = 0
    function increment() {
        value += i
        console.log(value)
    }
    return increment
}
const inc = createIncrement(10)
inc() // 10
inc() // 20
复制代码

createIncrement(10) 返回一个增量函数,该函数赋值给inc变量。当调用inc()时,value 变量加10。

第一次调用inc()返回10,第二次调用返回20,依此类推。

调用inc()时不带参数,JS仍然可以获取到当前 valuei的增量,来看看它是如何工作的。

原理就在 createIncrement() 中。当在函数上返回一个函数时,就会有闭包产生。闭包捕获了词法作用域中的变量 valuei

词法作用域是定义闭包的外部作用域。在本例中,increment() 的词法作用域是createIncrement()的作用域,其中包含变量 valuei

无论在何处调用 inc(),甚至在 createIncrement() 的作用域之外,它都可以访问 valuei

闭包是一个可以从其词法作用域记住和修改变量的函数,不管执行的作用域是什么。

二、React Hooks中的闭包

通过简化状态重用和副作用管理,Hooks 取代了基于类的组件。此外,咱们可以将重复的逻辑提取到自定义 Hook 中,以便在应用程序之间重用。Hooks严重依赖于 JS 闭包,但是闭包有时很棘手。

当咱们使用一个有多种副作用和状态管理的 React 组件时,可能会遇到的一个问题是过时的闭包,这可能很难解决。

三、过时的闭包

工厂函数createIncrement(i)返回一个increment函数。increment 函数对value增加i ,并返回一个记录当前value的函数

function createIncrement(i) {
    let value = 0
    function increment() {
        value += i
        console.log(value)
        const message = `Current value is ${value}`
        return function logValue() { // setState相当于logValue函数
            console.log(message)
        }
    }
    return increment
}
const inc = createIncrement(10)
const log = inc() // 10,将当前的value值固定
inc() // 20
inc() // 30

log() // "Current value is 10" 未能正确打印30
复制代码
function createIncrement(i) {
    let value = 0
    function increment() {
        value += i
        console.log(value)
        const message = `Current value is ${value}`
        return function logValue() { // setState相当于logValue函数
            console.log(message)
        }
    }
    return increment
}
const inc = createIncrement(1) // i被固定为1,输入几就被固定为几
inc() // 1
const log = inc() // 2
inc() // 3

log() // "Current value is 2" 未能正确打印3
复制代码

过时的闭包捕获具有过时值的变量

四、修复过时闭包的问题

(1) 使用新的闭包

解决过时闭包的第一种方法是找到捕获最新变量的闭包。

找到捕获了最新message变量的闭包,就是从最后一次调用inc()返回的闭包。

const inc = createIncrement(1)
inc() // 1
inc() // 2
const latestLog = inc()
latestLog() // "Current value is 3"
复制代码

以上就是React Hook处理闭包新鲜度的方法了。

Hooks实现假设在组件重新渲染之前,最为Hook回调提供的最新闭包(例如useEffect(callback))已经从组件的函数作用域捕获了最新的变量。也就是说在useEffect的第二个参数[]加入监听变化的值,在每次变化时,执行function,获取最新的闭包。

(2) 关闭已更改的变量

第二种方法是让logValue()直接使用 value

让我们移动行 const message = ...;logValue()函数体中:

function createIncrementFixed(i) {
  let value = 0;
  function increment() {
    value += i;
    console.log(value);
    return function logValue() {
      const message = `Current value is ${value}`;
      console.log(message);
    };
  }
  
  return increment;
}

const inc = createIncrementFixed(1);
const log = inc(); // 打印 1
inc();             // 打印 2
inc();             // 打印 3
// 正常工作
log();             // 打印 "Current value is 3"
复制代码

logValue()关闭createIncrementFixed()作用域内的value变量。log()现在打印正确的消息。

五、Hook中过时的闭包

useEffect()

在使用useEffect Hook时出现闭包的常见情况。

在组件WatchCount中,useEffect每秒打印count的值。

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)}> 加1 </button>
      </div>
    )
}
复制代码

点击几次加1按钮,我们从控制台看,每2秒打印的为Count is: 0

在第一渲染时,log()中闭包捕获count变量的值0。过后,即使count增加,log()中使用的仍然是初始化的值0log()中的闭包是一个过时的闭包。

解决方法:让useEffect()知道log()中的闭包依赖于count:

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]); // 看这里,这行是重点,count变化后重新渲染useEffect

  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1) }>
        Increase
      </button>
    </div>
  );
}
复制代码

设置依赖项后,一旦count更改,useEffect()就更新闭包。

正确管理 Hook 依赖关系是解决过时闭包问题的关键。推荐安装 eslint-plugin-react-hooks,它可以帮助咱们检测被遗忘的依赖项。

useState()

组件DelayedCount有 2 个按钮

  • 点击按键 “Increase async” 在异步模式下以1秒的延迟递增计数器
  • 在同步模式下,点击按键 “Increase sync” 会立即增加计数器
function DelayedCount() {
  const [count, setCount] = useState(0);

  function handleClickAsync() {
    setTimeout(function delay() {
      setCount(count + 1);
    }, 1000);
  }

  function handleClickSync() {
    
    setCount(count + 1);
  }

  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>Increase async</button>
      <button onClick={handleClickSync}>Increase sync</button>
    </div>
  )
}
复制代码

点击 “Increase async” 按键然后立即点击 “Increase sync” 按钮,count只更新到1

这是因为 delay() 是一个过时的闭包。

来看看这个过程发生了什么:

初始渲染:count 值为 0。 点击 'Increase async' 按钮。delay()闭包捕获 count 的值 0setTimeout() 1 秒后调用 delay()。 点击 “Increase async” 按键。handleClickSync() 调用 setCount(0 + 1)count的值设置为 1,组件重新渲染。 1 秒之后,setTimeout() 执行 delay() 函数。但是 delay() 中闭包保存count 的值是初始渲染的值 0,所以调用 setState(0 + 1),结果count保持为 1。

delay() 是一个过时的闭包,它使用在初始渲染期间捕获的过时的 count变量。

为了解决这个问题,可以使用函数方法来更新 count 状态:

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() 中的count 状态。React 确保将最新状态值作为参数提供给更新状态函数,过时的闭包的问题就解决了。

总结

闭包是一个函数,它从定义变量的地方(或其词法范围)捕获变量。

当闭包捕获过时的变量时,就会出现过时闭包的问题。

解决闭包的有效方法

  1. 正确设置React Hook 的依赖项
  2. 对于过时的状态,使用函数方式更新状态
这篇关于React Hooks 中的闭包问题的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!