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

How to create a stale closure like the one in React's useEffect hook without using the actual useEffect hook?

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

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

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

What am I doing wrong? What should I do to create a stale closure like the one we get in React's useEffect when we don't provide the complete dependencies array?

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

为了重新渲染一个功能性的 React 组件,React 再次调用组件函数。这将创建一个新范围,该范围与之前调用函数组件时创建的范围没有 link。

要获得过时的关闭,您需要多次调用 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);

Why does a stale closure is created when we don't give exhaustive dependencies in a useEffect? Why doesn't the code in a useEffect hook's callback just use the lexical scope, like a normal function would, and print the actual value?

上面的代码示例应该让您了解为什么会创建陈旧的闭包。 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);