什么是使用影响反应挂钩中的执行顺序及其内部清理逻辑?

hij*_*tao 9 javascript frontend reactjs react-hooks

根据反应文件,useEffect在重新运行useEffect部分之前将触发清理逻辑.

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

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

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

我不知道反应钩子在执行清理代码之前是否会做一些额外的事情,这使得我的取消动画部分不能正常工作,会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"));
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

Kyl*_*son 12

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

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

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

  useLayoutEffect(() => {
    console.log('useLayoutEffect')
    return () => {
      console.log('useLayoutEffect cleanup')
    }
  })
Run Code Online (Sandbox Code Playgroud)

useLayoutEffect > requestAnimationFrame > useEffect

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

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

更改useEffectuseLayoutEffect它应该可以解决您的问题。

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

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

  useEffect(() => {
    console.log('useEffect-1')
  })
  useEffect(() => {
    console.log('useEffect-2')
  })
  useLayoutEffect(() => {
    console.log('useLayoutEffect-1')
  })
  useLayoutEffect(() => {
    console.log('useLayoutEffect-2')
  })
Run Code Online (Sandbox Code Playgroud)


Shu*_*tri 6

在使用钩子和尝试实现生命周期功能时,您需要关注两种不同的钩子。

根据文档:

useEffect在 react 渲染您的组件后运行,并确保您的效果回调不会阻止浏览器绘制。这不同于类组件的行为,其中componentDidMountcomponentDidUpdate渲染后同步运行。

因此requestAnimationFrame在这些生命周期中使用可以无缝地工作,但在useEffect. 因此,当您必须进行的更改不会阻止视觉更新(例如在收到响应后进行导致 DOM 更改的 API 调用)时,应该使用 useEffect。

另一个不太流行但在处理可视化 DOM 更新时非常方便的钩子是useLayoutEffect. 根据文档

签名与 useEffect 相同,但在所有 DOM 突变后同步触发。使用它从 DOM 读取布局并同步重新渲染。useLayoutEffect在浏览器有机会绘制之前,内部计划的更新将同步刷新。

因此,如果您的效果正在改变 DOM(通过 DOM 节点引用)并且 DOM 突变将在渲染它和您的效果改变它之间改变 DOM 节点的外观,那么您不想使用useEffect. 你会想要使用useLayoutEffect. 否则,当您的 DOM 更改生效时,用户可能会看到闪烁,这正是requestAnimationFrame

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

const {useState, useLayoutEffect} = React;

//import ReactDOM from "react-dom";

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

  useLayoutEffect(() => {
    setStartSeconds(Math.random());

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

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

  useLayoutEffect(
    () => {
      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"));
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)


Aid*_*ane 5

在上面的答案中还不清楚的一件事是,当您混合使用多个成分时,效果的运行顺序。我们一直在进行涉及通过useContext协调父级和子级之间的工作,因此顺序对我们而言更为重要。useLayoutEffect并且useEffect在这方面以不同的方式工作。

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
Run Code Online (Sandbox Code Playgroud)
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 <></>
}
Run Code Online (Sandbox Code Playgroud)

  • 从 React 17 开始,这是不正确的。如 https://codesandbox.io/s/sad-brown-7xdfgw?file=/src/App.js 中所示,useEffect 清理全部在下一个效果之前运行,而不会交错。[发行说明](https://legacy.reactjs.org/blog/2020/10/20/react-v17.html) 中也提到了这一点(“在运行任何下一个效果之前清理所有效果。”) 。 (3认同)