如何在不使用实际的 useEffect 钩子的情况下创建像 React 的 useEffect 钩子中那样的陈旧闭包?

时间:2021-07-27 13:02:26

标签: javascript reactjs react-hooks closures

我知道闭包是如何工作的,但我无法理解在没有详尽的依赖项数组的情况下如何在 React 的 useEffect 中创建陈旧的闭包。为此,我试图复制一个陈旧的闭包,就像在 React 的 useEffect 中一样,而不使用 useEffect,但我无法创建它。我的代码不会创建陈旧的闭包,而是在每个间隔上记录一个正确的值。请您看一下下面的片段并告诉我:

  1. 我做错了什么?当我们不提供完整的依赖项数组时,我应该怎么做才能创建一个像我们在 React 的 useEffect 中得到的那样的陈旧闭包? (参考代码在文末)

  2. 当我们没有在 useEffect 中给出详尽的依赖时,为什么会创建一个陈旧的闭包?为什么 useEffect 钩子回调中的代码不像普通函数那样只使用词法作用域,并打印实际值?

function createIncrement(incBy) {
  let value = 0;

  function increment() {
    value += incBy;
    console.log(value);
  }
  
  function useEffect(fun) {
    fun()
  }
  
  useEffect(function() {
    setInterval(function log() {
          // what should I do to create a stale closure here?
          // So that if I change the value it should show me the old value
          // as it does when using React's useEffect without exhaustive dependencies array

          console.log(`Count is: ${value}`); // prints correct value each time
        }, 2000);
  });
  
  setTimeout(() => {
    increment(); // increments to 5
    increment(); // increments to 6
  }, 5000);
  
  return [increment];
}

const [increment] = createIncrement(1);
increment(); // increments to 1
increment(); // increments to 2
increment(); // increments to 3
increment(); // increments to 4

为了完整起见,下面是一个使用 React 的 useEffect 的代码片段,其中我们没有为 React 的 useEffect 提供详尽的依赖数组,因此创建了一个陈旧的闭包:

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function WatchCount() {
  const [count, setCount] = useState(0);

  useEffect(function () {
    setInterval(function log() {
      // No matter how many times you increase the counter 
      // by pressing the button below,
      // this will always log count as 0
      console.log(`Count is: ${count}`);
    }, 2000);
  }, []);

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

const rootElement = document.getElementById("root");
ReactDOM.render(<WatchCount />, rootElement);

1 个答案:

答案 0 :(得分:1)

<块引用>

我做错了什么?我应该怎么做才能创建一个陈旧的闭包,例如 当我们不提供完整的时,我们在 React 的 useEffect 中得到的 依赖数组?

您不会得到过时的闭包,因为您只调用了一次 createIncrement 函数。

为了重新渲染一个功能性的react组件,react再次调用组件函数。这将创建一个新的作用域,该作用域与先前调用函数组件时创建的作用域没有链接。

要获得过时的闭包,您需要多次调用 createIncrement

function createIncrement(incBy, functionCallIdentifier, intervalDelay) {
  let value = 0;

  function increment() {
    value += incBy;
    console.log("inside call: " + functionCallIdentifier + " - value: " + value);
  }

  increment();
  
  function useEffect(fun) {
    fun();
  }

  useEffect(function () {
    setInterval(function log() {
      console.log("inside call: " + functionCallIdentifier + " - count: " + value);
    }, intervalDelay);
  });
}

createIncrement(1, "first call", 2000);

// calling again but 'setInterval' set in the first call will continue
// to log the latest value of variable "value" that it closed over, i.e. 1 
createIncrement(2, "second call", 4000);

<块引用>

当我们没有给出详尽的说明时,为什么会创建一个陈旧的闭包 useEffect 中的依赖项?为什么 useEffect 中的代码没有 hook 的回调只使用词法作用域,就像普通函数一样 会,并打印实际值?

上面的代码示例应该让您了解为什么会创建过时的闭包。 useEffect 的回调函数确实使用了词法作用域,但不同的是再次调用函数组件会创建一个新的作用域,但在组件的先前渲染中设置的 useEffect 钩子的旧回调函数将继续查看其创建范围内的值。

此行为并非特定于 react - 这是 JavaScript 中每个函数的行为方式:每个函数都在创建它的范围内关闭。

直到在 useEffect 钩子中设置新回调之前清除前一个回调,旧回调将继续记录其关闭范围内的值。

以下代码示例使用清理函数清除第一次调用 createIncrement 函数时设置的间隔。

function createIncrement(incBy, functionCallIdentifier, useEffectCleanupFn) {
  if (useEffectCleanupFn) {
    useEffectCleanupFn();
  }

  let value = 0;

  function increment() {
    value += incBy;
    console.log("inside call: " + functionCallIdentifier + " - value: " + value);
  }

  increment();
  
  let cleanupFn;

  function useEffect(fun) {
    cleanupFn = fun();
  }

  useEffect(function () {
    let id = setInterval(function log() {
      console.log("inside call: " + functionCallIdentifier + " - count: " + value);
    }, 2000);

    return () => clearInterval(id);
  });

  return cleanupFn;
}

let cleanupFn1 = createIncrement(1, "first call");

setTimeout(() => {
  createIncrement(2, "second call", cleanupFn1);
}, 4000);