React Hooks中useEffect的执行顺序和它内部的清理逻辑是什么?

新手上路,请多包涵

根据 React 文档, useEffect 将在重新运行之前触发清理逻辑 useEffect 部分。

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

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

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

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

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

谢谢。

 //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>

原文由 hijiangtao 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1k
2 个回答

将这三行代码放在一个组件中,您将看到它们的优先级顺序。

   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 > useEffect

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

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

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

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

您可以通过运行以下行来查看:

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

原文由 Kyle Richardson 发布,翻译遵循 CC BY-SA 4.0 许可协议

上述答案中不清楚的一件事是当您在混音中有多个组件时效果运行的顺序。我们一直在做的工作涉及通过 useContext 在父母和孩子之间进行协调,因此顺序对我们来说更重要。 useLayoutEffectuseEffect 在这方面以不同的方式工作。

useEffect 在移动到下一个组件(深度优先)并执行相同操作之前运行清理和新效果。

useLayoutEffect 运行每个组件的清理(深度优先),然后运行所有组件的新效果(深度优先)。

 render parent
render a
render b
layout cleanup a
layout cleanup b
layout cleanup parent
layout effect a
layout effect b
layout effect parent
effect cleanup a
effect a
effect cleanup b
effect b
effect cleanup parent
effect parent

 const Test = (props) => {
  const [s, setS] = useState(1)

  console.log(`render ${props.name}`)

  useEffect(() => {
    const name = props.name
    console.log(`effect ${props.name}`)
    return () => console.log(`effect cleanup ${name}`)
  })

  useLayoutEffect(() => {
    const name = props.name
    console.log(`layout effect ${props.name}`)
    return () => console.log(`layout cleanup ${name}`)
  })

  return (
    <>
      <button onClick={() => setS(s+1)}>update {s}</button>
      <Child name="a" />
      <Child name="b" />
    </>
  )
}

const Child = (props) => {
  console.log(`render ${props.name}`)

  useEffect(() => {
    const name = props.name
    console.log(`effect ${props.name}`)
    return () => console.log(`effect cleanup ${name}`)
  })

  useLayoutEffect(() => {
    const name = props.name
    console.log(`layout effect ${props.name}`)
    return () => console.log(`layout cleanup ${name}`)
  })

  return <></>
}

原文由 Aidan Kane 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题