繁体   English   中英

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

[英]What's useEffect execution order and its internal clean-up logic in react hooks?

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

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

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

但是,当我在useEffect使用requestAnimationFramecancelAnimationFrame ,我发现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")); 
 <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> 

在上面的答案中不清楚的一件事是当混合中有多个组件时效果运行的顺序。 我们一直在通过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 <></>
}

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

根据文档:

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

因此在这些生命周期中使用requestAnimationFrame可以useEffect地工作,但是使用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")); 
 <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> 

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

  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之前调用,并且在下次执行之前调用它的清理函数可以防止重叠。

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM