所有分类
  • 所有分类
  • 后端开发

React每日一学:了解 React Hooks 闭包的应用和原理

今日分享下React知识点:了解 React Hooks 闭包的应用和原理。

React Hooks 是 React 16.8 版本引入的一种新的特性,它允许我们在不编写 class 组件的情况下使用 state 以及其他的 React 功能。其中,最为常用的就是 useState 和 useEffect。在使用 React Hooks 时,由于函数组件没有实例,所以 Hooks 靠的是闭包来访问和更新 state。但是,在使用 Hooks 时,我们需要注意闭包陷阱问题。

什么是闭包陷阱?

闭包是指一个函数可以访问定义在函数外部的变量。在 React 中,Hooks 函数也是闭包,它们可以访问定义在函数外部的变量。React Hooks 的闭包陷阱与普通 JavaScript 中的闭包陷阱类似,但是由于 React Hooks 的设计,使用 Hooks 时可能会遇到一些特定的问题。

React Hooks 中的闭包陷阱主要会发生在两种情况:

  • 在 useState 中使用闭包;

  • 在 useEffect 中使用闭包。

useState 中的闭包陷阱

在useState中使用闭包,主要是因为useState的参数只会在组件挂载时执行一次。如果我们在useState中使用闭包,那么闭包中的变量值会被缓存,这意味着当我们在组件中更新状态时,闭包中的变量值不会随之更新。

示例

React Hooks 的闭包陷阱发生在 useState 钩子函数中的示例,如下:

function Counter() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setTimeout(() => {
      setCount(count + 1);
    }, 1000);
  };
  const handleReset = () => {
    setCount(0);
  };
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
      <button onClick={handleReset}>Reset</button>
    </div>
  );
}

在上面的代码中,我们定义了一个handleClick函数,它使用了一个闭包来缓存count的值。然而,由于闭包中的count值被缓存了,这意味着即使我们在1秒后调用setCount方法来更新count的值,闭包中的count值仍然是旧的值。因此,如果我们点击Increment按钮,即使我们重复点击多次,计数器也只会增加1次。

避免方法

为了解决这个问题,我们需要使用React Hooks提供的更新函数的形式来更新状态。我们可以把handleClick函数改成这样:

const handleClick = () => {
  setTimeout(() => {
    setCount(count => count + 1);
  }, 1000);
};

在这个版本的handleClick函数中,我们使用了setCount的更新函数形式。这个函数会接收count的当前值作为参数,这样我们就可以在闭包中使用这个值,而不需要担心它被缓存。

useEffect 的闭包陷阱

在useEffect中使用闭包的问题则是因为useEffect中的函数是在每次组件更新时都会执行一次。如果我们在useEffect中使用闭包,那么这个闭包中的变量值也会被缓存,这样就可能会导致一些问题。

示例

React Hooks 中的闭包陷阱通常发生在 useEffect 钩子函数中,例如:

function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count);
    }, 1000);
    return () => clearInterval(timer);
  }, []);
  const handleClick = () => {
    setCount(count + 1);
  };
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

在这个例子中,我们使用了 useState 和 useEffect Hooks。在 useEffect 回调函数内部,我们使用了一个 setTimeout 函数来更新 count 状态变量。然而,由于 useEffect 只会在组件首次渲染时执行一次,因此闭包中的 count 变量始终是首次渲染时的变量,而不是最新的值。

避免方法

为了避免这种闭包陷阱,可以使用 useEffect Hook 来更新状态。例如,以下代码中,通过 useEffect Hook 来更新 count 的值,就可以避免闭包陷阱:

useEffect(() => {
  const timer = setInterval(() => {
    console.log(count);
  }, 1000);
  return () => clearInterval(timer);
}, [count]);

通过闭包访问和更新 state

在 React 中,class 组件可以使用 this.state 和 this.setState 来管理组件的状态。这是因为 class 组件具有实例,可以将状态存储在实例属性中,以便在组件的生命周期方法和事件处理程序中访问和更新。

而函数组件则没有实例,无法将状态存储在实例属性中。为了解决这个问题,React 引入了 React Hooks,其中最为常用的是 useState。useState 允许我们在函数组件中使用 state,而无需编写 class 组件。

useState 是通过闭包来实现的。当我们调用 useState 时,它会返回一个数组,其中第一个元素是当前状态的值,第二个元素是更新状态的函数。例如:

import React, { useState } from 'react';
const Counter = () => {
  const [count, setCount] = useState(0);
  // ...
};

在这个例子中,useState 的初始值为 0,useState 的返回值是一个数组 [count, setCount],其中 count 是当前状态的值,setCount 是更新状态的函数。

当我们在组件内部调用 setCount 函数时,React 会在内部使用闭包来访问和更新 count 变量。这是因为,useState 是在组件的顶层作用域中调用的,而 setCount 函数是在组件的事件处理程序中调用的。这意味着,setCount 函数需要访问 count 变量,但是 count 变量无法存储在实例属性中。

为了解决这个问题,React 使用了闭包,将 count 变量保存在内部函数中。当组件重新渲染时,React 会创建一个新的闭包,并将 count 变量的值更新为新的状态值。这个新的闭包会在下一次调用 setCount 函数时被使用。

下面是一个例子,展示了 useState 如何通过闭包来访问和更新 state 的:

import React, { useState } from 'react';
const Counter = () => {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };
  return (
    <>
      <p>You clicked {count} times</p>
      <button onClick={handleClick}>Click me</button>
    </>
  );
};

在这个例子中,我们调用 useState,并将初始值设置为 0。在组件内部,我们创建了一个 handleClick 函数,并调用 setCount 函数来更新 count 的值。由于 setCount 函数是在 handleClick 函数中调用的,因此需要使用闭包来访问和更新 count 变量。

需要注意的是,由于闭包的作用,如果我们在组件的事件处理程序中访问了过时的 state,可能会导致组件的状态出现错误。为了避免这种情况,我们需要使用 React Hooks 提供的其他功能,例如 useEffect 和 useCallback。这些功能可以帮助我们避免闭包陷阱,确保组件的状态更新正确地渲染到视图上。

从 React Hooks 源码看闭包陷阱

React Hooks 中闭包陷阱的问题源于 useState 等 Hooks 的实现方式。在 React 内部,每个组件都有一个对应的 Fiber 对象,表示组件的渲染状态。useState 等 Hooks 的实现都是基于这个 Fiber 对象的,并且会在 Fiber 对象中存储当前状态值和更新状态的函数。

例如,在 useState Hook 中,会通过调用 useStateImpl 函数来获取当前状态值和更新状态的函数:

function useState(initialState) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

function useStateImpl(initialState) {
  const hook = mountState(initialState);
  return [hook.memoizedState, dispatchAction.bind(null, hook.queue)];
}

其中,mountState 函数是用来初始化 Hook 对象的。它会检查当前 Fiber 对象上是否已经存在对应的 Hook,如果存在的话就直接返回该 Hook,否则就创建一个新的 Hook 对象并存储到当前 Fiber 对象上:

function mountState(initialState) {
  const currentHook = updateQueue.next;
  if (currentHook !== null) {
    updateQueue.next = currentHook.next;
    return currentHook;
  } else {
    const newHook = {
      memoizedState: typeof initialState === 'function' ? initialState() : initialState,
      queue: [],
      next: null,
    };
    if (updateQueue.last === null) {
      updateQueue.first = updateQueue.last = newHook;
    } else {
      updateQueue.last = updateQueue.last.next = newHook;
    }
    return newHook;
  }
}

需要注意的是,每个 Hook 对象中都有一个 queue 属性,用来存储更新状态的 action。而 dispatchAction 函数则是用来触发更新的:

function dispatchAction(queue, action) {
  const update = {
    action,
    next: null,
  };
  if (queue.last === null) {
    queue.first = queue.last = update;
  } else {
    queue.last = queue.last.next = update;
  }
  scheduleWork();
}

在组件重新渲染时,React 会重新执行函数组件的函数体,从而调用 useState 等 Hook 重新获取状态值和更新状态的函数。由于每次重新渲染都会创建一个新的 Fiber 对象,因此在新的 Fiber 对象上获取到的 Hook 对象和状态值都是新的。

然而,由于更新状态的函数是存储在 Hook 对象中的,因此会造成更新函数的闭包引用的是旧的状态值,而不是最新的状态值。例如,在以下代码中,每次点击按钮都会增加 count 的值,但是打印出来的 count 值却始终为 1,这是因为 setCount 使用的是 count 的初始值,而不是最新的值,因为 setCount 是在一个闭包中定义的:

function Counter() {
  let count = 0;
  const [visible, setVisible] = useState(false);
  function handleClick() {
    count++;
    console.log(count);
    setVisible(!visible);
  }
  return (
    <>
      <button onClick={handleClick}>Click me</button>
      {visible && <div>Count: {count}</div>}
    </>
  );
}

原文链接:https://www.icz.com/technicalinformation/web/seo/2023/05/9688.html,转载请注明出处~~~
0
注意:请收藏好网址www.icz.com,防止失联!站内免费资源持续上传中…!赞助我们
显示验证码
没有账号?注册  忘记密码?