react 钩子中的 useEffect 执行顺序及其内部清理逻辑是什么?

2022-01-19 00:00:00 reactjs frontend javascript react-hooks

根据react文档,useEffect会在重新运行useEffect部分之前触发清理逻辑.

According to react document, useEffect will trigger clean-up logic before it re-runs useEffect part.

如果你的效果返回一个函数,React 会在需要清理的时候运行它...

If your effect returns a function, React will run it when it is time to clean up...

没有用于处理更新的特殊代码,因为 useEffect 默认处理它们.它会在应用下一个效果之前清理之前的效果...

There is no special code for handling updates because useEffect handles them by default. It cleans up the previous effects before applying the next effects...

但是,当我在 useEffect 中使用 requestAnimationFramecancelAnimationFrame 时,我发现 cancelAnimationFrame 可能无法正常停止动画.有时,我发现旧动画仍然存在,而下一个效果带来另一个动画,这导致我的 Web 应用程序性能问题(尤其是当我需要渲染大量 DOM 元素时).

However, when I use requestAnimationFrame and cancelAnimationFrame inside useEffect, I found the cancelAnimationFrame may not stop the animation normally. Sometimes, I found the old animation still exists, while the next effect brings another animation, which causes my web app performance issues (especially when I need to render heavy DOM elements).

我不知道 react 钩子在执行清理代码之前是否会做一些额外的事情,这会使我的取消动画部分无法正常工作,useEffect 钩子会做一些类似于闭包的事情锁定状态变量?

I don't know whether react hook will do some extra things before it executes the clean-up code, which make my cancel-animation part not work well, will useEffect hook do something like closure to lock the state variable?

useEffect 的执行顺序和内部清理逻辑是什么?是不是我下面写的代码有问题,导致cancelAnimationFrame不能正常工作?

What's useEffect's execution order and its internal clean-up logic? Is there something wrong the code I write below, which makes cancelAnimationFrame can't work perfectly?

谢谢.

//import React, { useState, useEffect } from "react";

const {useState, useEffect} = React;

//import ReactDOM from "react-dom";

function App() {
  const [startSeconds, setStartSeconds] = useState(Math.random());
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setStartSeconds(Math.random());
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  useEffect(
    () => {
      let raf = null;

      const onFrame = () => {
        const currentProgress = startSeconds / 120.0;
        setProgress(Math.random());
        // console.log(currentProgress);
        loopRaf();
        if (currentProgress > 100) {
          stopRaf();
        }
      };

      const loopRaf = () => {
        raf = window.requestAnimationFrame(onFrame);
        // console.log('Assigned Raf ID: ', raf);
      };

      const stopRaf = () => {
        console.log("stopped", raf);
        window.cancelAnimationFrame(raf);
      };

      loopRaf();

      return () => {
        console.log("Cleaned Raf ID: ", raf);
        // console.log('init', raf);
        // setTimeout(() => console.log("500ms later", raf), 500);
        // setTimeout(()=> console.log('5s later', raf), 5000);
        stopRaf();
      };
    },
    [startSeconds]
  );

  let t = [];
  for (let i = 0; i < 1000; i++) {
    t.push(i);
  }

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <text>{progress}</text>
      {t.map(e => (
        <span>{progress}</span>
      ))}
    </div>
  );
}

ReactDOM.render(<App />,
document.querySelector("#root"));

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>

推荐答案

将这三行代码放在一个组件中,你会看到它们的优先顺序.

Put these three lines of code in a component and you'll see their order of priority.

  useEffect(() => {
    console.log('useEffect')
    return () => {
      console.log('useEffect cleanup')
    }
  })

  window.requestAnimationFrame(() => console.log('requestAnimationFrame'))

  useLayoutEffect(() => {
    console.log('useLayoutEffect')
    return () => {
      console.log('useLayoutEffect cleanup')
    }
  })

useLayoutEffect >requestAnimationFrame >使用效果

您遇到的问题是由 loopRaf 在执行 useEffect 的清理功能之前请求另一个动画帧引起的.

The problem you're experiencing is caused by loopRaf requesting another animation frame before the cleanup function for useEffect is executed.

进一步的测试表明,useLayoutEffect 总是在 requestAnimationFrame 之前调用,并且它的清理函数在下次执行之前调用,以防止重叠.

Further testing has shown that useLayoutEffect is always called before requestAnimationFrame and that its cleanup function is called before the next execution preventing overlaps.

useEffect 更改为 useLayoutEffect 它应该可以解决您的问题问题.

Change useEffect to useLayoutEffect and it should solve your problem.

useEffectuseLayoutEffect 是按照它们在代码中出现的顺序被调用的,就像 useState 调用一样.

useEffect and useLayoutEffect are called in the order they appear in your code for like types just like useState calls.

您可以通过运行以下几行来看到这一点:

You can see this by running the following lines:

  useEffect(() => {
    console.log('useEffect-1')
  })
  useEffect(() => {
    console.log('useEffect-2')
  })
  useLayoutEffect(() => {
    console.log('useLayoutEffect-1')
  })
  useLayoutEffect(() => {
    console.log('useLayoutEffect-2')
  })

相关文章