reactjs 为什么每次渲染都调用'useEffect'中的cleanup函数?

vngu2lb8  于 2023-02-08  发布在  React
关注(0)|答案(6)|浏览(259)

我一直在学习React,我读到从useEffect返回的函数是用来做清理的,React在组件卸载时执行清理。
因此,我对其进行了一些试验,但在下面的示例中发现,每次组件重新呈现时都会调用该函数,而不是仅在组件从DOM卸载时调用,即每次组件重新呈现时调用console.log("unmount");
为什么会这样?

function Something({ setShow }) {
  const [array, setArray] = useState([]);
  const myRef = useRef(null);

  useEffect(() => {
    const id = setInterval(() => {
      setArray(array.concat("hello"));
    }, 3000);
    myRef.current = id;
    return () => {
      console.log("unmount");
      clearInterval(myRef.current);
    };
  }, [array]);

  const unmount = () => {
    setShow(false);
  };

  return (
    <div>
      {array.map((item, index) => {
        return (
          <p key={index}>
            {Array(index + 1)
              .fill(item)
              .join("")}
          </p>
        );
      })}
      <button onClick={() => unmount()}>close</button>
    </div>
  );
}

function App() {
  const [show, setShow] = useState(true);

  return show ? <Something setShow={setShow} /> : null;
}

示例:https://codesandbox.io/s/vigilant-leavitt-z1jd2

bsxbgnwa

bsxbgnwa1#

React在卸载组件时执行清理。
我不知道你从哪里读到的,但这句话是不正确的。当钩子的依赖关系改变,效果钩子需要用新的值再次运行时,React会执行清理。这种行为是故意为了保持视图对变化数据的React。离开官方的例子,让我们假设一个应用从朋友的个人资料订阅状态更新。作为一个伟大的朋友,你是,你决定与他们解除好友关系并与其他人建立好友关系。现在,应用需要取消订阅以前好友的状态更新,并听取新好友的更新。这是很自然的,而且很容易通过useEffect的工作方式实现。

useEffect(() => { 
    chatAPI.subscribe(props.friend.id);

    return () => chatAPI.unsubscribe(props.friend.id);
  }, [ props.friend.id ])

通过在依赖列表中包含朋友id,我们可以指示钩子只需要在朋友id改变时运行。
在你的例子中,你已经在依赖列表中指定了array,并且你在一个设定的时间间隔内改变数组,每次你改变数组,钩子都会重新运行。
只需从依赖项列表中删除数组并使用回调版本的setState钩子就可以实现正确的功能。回调版本总是在状态的前一版本上操作,因此不需要在每次数组更改时刷新钩子。

useEffect(() => {
    const id = setInterval(() => setArray(array => [ ...array, "hello" ]), 3000);

    return () => {
      console.log("unmount");
      clearInterval(id);
    };
  }, []);

一些额外的反馈是直接在clearInterval中使用id,因为在创建cleanup函数时,该值会被关闭(捕获),没有必要将其保存到ref中。

lh80um4z

lh80um4z2#

React文档中有一个解释部分。
简而言之,原因是这样的设计可以防止陈旧数据和更新错误。
React中的useEffect钩子设计用于处理初始渲染和任何后续渲染(here's more about it)。
效果是通过其依赖项控制的,而不是通过使用它们的组件的生命周期控制的。
任何时候效果的依赖关系改变,useEffect将清除以前的效果并运行新的效果。
这样的设计更具可预测性-each render has its own independent (pure) behavioral effect。这确保了UI总是显示正确的数据(因为React的心智模型中的UI是特定渲染状态的屏幕截图)。
我们控制效果的方法是通过它们的依赖性。
为了防止清理在每个渲染上运行,我们只需要不改变效果的依赖关系。
具体来说,在您的示例中,由于array正在更改(即Object.is(oldArray, newArray) === false),因此正在进行清理

useEffect(() => {
  // ...
}, [array]);
//  ^^^^^ you're changing the dependency of the effect

您将使用以下行引起此更改:

useEffect(() => {
  const id = setInterval(() => {
    setArray(array.concat("hello")); // <-- changing the array changes the effect dep
  }, 3000);
  myRef.current = id;

  return () => {
    clearInterval(myRef.current);
  };
}, [array]); // <-- the array is the effect dep
qlckcl4x

qlckcl4x3#

正如其他人所说,useEffect依赖于useEffect第二个参数中指定的"array"的变化,因此通过将其设置为空数组,这将有助于在组件挂载时触发useEffect一次。
此处的技巧是更改Array的先前状态。

setArray((arr) => arr.concat("hello"));

见下文:

useEffect(() => {
     const id = setInterval(() => {
         setArray((arr) => arr.concat("hello"));
     }, 3000);
     myRef.current = id;
     return () => {
        console.log("unmount");
        clearInterval(myRef.current);
     };
  }, []);

我派生了您的CodeSandbox用于演示:https://codesandbox.io/s/heuristic-maxwell-gcuf7?file=/src/index.js

o75abkj4

o75abkj44#

查看代码,我可以猜到这是因为第二个参数[array]。您正在更新它,所以它将调用一个重新呈现。尝试设置一个空数组。
每次状态更新都将调用重新渲染和卸载,并且该数组正在更改。

fykwrbwg

fykwrbwg5#

这似乎是意料之中的。根据这里的文档,useEffect在第一次渲染,每次更新和卸载后被调用。
https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
头端
如果您熟悉React类生命周期方法,您可以将useEffect Hook看作componentDidMount、componentDidUpdate和componentWillUnmount之前的组合。

e4yzc0pl

e4yzc0pl6#

这是一个显示渲染和效果顺序的Jest测试。
正如您可以从expect中看到的,一旦依赖项foo由于状态更新而更改,它将触发一个NEW渲染,然后是第一个渲染的cleanup函数。

it("with useEffect async set state and timeout and cleanup", async () => {
    jest.useFakeTimers();
    let theRenderCount = 0;
    const trackFn = jest.fn((label: string) => { });
    function MyComponent() {
      const renderCount = theRenderCount;
      const [foo, setFoo] = useState("foo");
      useEffect(() => {
        trackFn(`useEffect ${renderCount}`);
        (async () => {
          await new Promise<string>((resolve) =>
            setTimeout(() => resolve("bar"), 5000)
          );
          setFoo("bar");
        })();
        return () => trackFn(`useEffect cleanup ${renderCount}`);
      }, [foo]);
      ++theRenderCount;
      trackFn(`render ${renderCount}`);
      return <span data-testid="asdf">{foo}</span>;
    }
    const { unmount } = render(<MyComponent></MyComponent>);
    expect(screen.getByTestId("asdf").textContent).toBe("foo");
    jest.advanceTimersByTime(4999);
    expect(screen.getByTestId("asdf").textContent).toBe("foo");
    jest.advanceTimersByTime(1);
    await waitFor(() =>
      expect(screen.getByTestId("asdf").textContent).toBe("bar")
    );

    trackFn("before unmount");
    unmount();
    expect(trackFn.mock.calls).toEqual([
      ['render 0'],
      ['useEffect 0'],
      ['render 1'],
      ['useEffect cleanup 0'],
      ['useEffect 1'],
      ['before unmount'],
      ['useEffect cleanup 1']
    ])
  });

相关问题