React Hooks 中的闭包问题

前言

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

1、从JS中的闭包提及

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

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

关于词法做用域相关的知识点,能够查阅《你不知道的JavaScript》找到答案。闭包

React Hooks中的闭包和咱们在JS中见到的闭包并没有不一样。异步

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

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,依此类推。ui

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

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

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

不管在何处调用 inc(),甚至在 createIncrement() 的做用域以外,它均可以访问 valuei

闭包是一个能够从其词法做用域记住和修改变量的函数,无论执行的做用域是什么。

2、React Hooks中的闭包

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

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

3、过期的闭包

工厂函数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
复制代码

过期的闭包捕获具备过期值的变量

4、修复过期闭包的问题

(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()如今打印正确的消息。

5、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. 对于过期的状态,使用函数方式更新状态
相关文章
相关标签/搜索