react 钩子中的 useEffect 执行顺序及其内部清理逻辑是啥?
Posted
技术标签:
【中文标题】react 钩子中的 useEffect 执行顺序及其内部清理逻辑是啥?【英文标题】:What's useEffect execution order and its internal clean-up logic in react hooks?react 钩子中的 useEffect 执行顺序及其内部清理逻辑是什么? 【发布时间】:2019-05-15 20:05:31 【问题描述】:根据反应文档,useEffect
将在重新运行useEffect
部分之前触发清理逻辑。
如果你的效果返回一个函数,React 会在需要清理的时候运行它...
没有处理更新的特殊代码,因为
useEffect
默认处理它们。它会在应用下一个效果之前清理之前的效果...
但是,当我在useEffect
中使用requestAnimationFrame
和cancelAnimationFrame
时,我发现cancelAnimationFrame 可能无法正常停止动画。有时,我发现旧动画仍然存在,而下一个效果带来另一个动画,这导致我的 Web 应用程序性能问题(尤其是当我需要渲染大量 DOM 元素时)。
我不知道 react 钩子在执行清理代码之前是否会做一些额外的事情,这使得我的取消动画部分不能正常工作,useEffect
钩子会做一些类似于闭包的事情来锁定状态变量?
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>
【问题讨论】:
您是否尝试对类组件做同样的事情?结果不同吗?useEffect(() => setStartSeconds(Math.random());
你在创建效果时导致了更新,我很惊讶你没有得到一个无限循环,我认为 React 在这里保护你。
@Keith 他有一个空数组作为useEffect
调用的第二个参数,这使得它只在挂载和卸载时执行。该块只是初始化和清理。话虽如此,他本可以使用该值初始化 useState
调用。
@KyleRichardson 但是他的第二个效果有[startSeconds]
,所以无缘无故地有一个额外的渲染,正如他应该做的那样 -> const [startSeconds, setStartSeconds] = useState(Math.random());
这可能不是问题的原因,但是对我来说,在效果初始化中改变状态感觉不对。
@Keith Ya 我知道,但这不是整个问题的原因。我同意这是不需要的重新渲染,应该在使用状态下初始化;但他遇到的问题是因为他在应该使用useLayoutEffect
时使用useEffect
。
【参考方案1】:
将这三行代码放在一个组件中,您将看到它们的优先顺序。
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
应该可以解决您的问题 问题。
useEffect
和 useLayoutEffect
按照它们在代码中出现的顺序被调用,用于类似的类型,就像 useState
调用一样。
您可以通过运行以下几行来看到这一点:
useEffect(() =>
console.log('useEffect-1')
)
useEffect(() =>
console.log('useEffect-2')
)
useLayoutEffect(() =>
console.log('useLayoutEffect-1')
)
useLayoutEffect(() =>
console.log('useLayoutEffect-2')
)
【讨论】:
非常感谢,我似乎没有注意到useEffect
和useLayoutEffect
之间的用法差异,它现在对我有用。
喜欢这个答案,但希望得到文档或其他可靠参考资料的支持(如果可能的话)。谢谢!【参考方案2】:
在使用钩子并尝试实现生命周期功能时,您需要关注两种不同的钩子。
根据文档:
useEffect
在 react 渲染你的组件后运行,并确保 您的效果回调不会阻止浏览器绘画。这不同 从componentDidMount
和componentDidUpdate
渲染后同步运行。
因此在这些生命周期中使用requestAnimationFrame
可以无缝工作,但与useEffect
有一点小故障。因此 useEffect 应该用于当您必须进行的更改不会阻止视觉更新时,例如在收到响应后进行导致 DOM 更改的 API 调用。
另一个不太流行但在处理可视 DOM 更新时非常方便的钩子是useLayoutEffect
。 As per the docs
签名与 useEffect 相同,但它是同步触发的 毕竟DOM突变。使用它从 DOM 中读取布局并 同步重新渲染。
useLayoutEffect
内安排的更新将 在浏览器有机会绘制之前同步刷新。
因此,如果您的效果正在改变 DOM(通过 DOM 节点 ref)并且 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
和useLayoutEffect
的用法区别,抱歉!【参考方案3】:
上述答案中不清楚的一点是,当您在混合中有多个组件时,效果的运行顺序。我们一直在做涉及通过 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
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 <></>
【讨论】:
以上是关于react 钩子中的 useEffect 执行顺序及其内部清理逻辑是啥?的主要内容,如果未能解决你的问题,请参考以下文章
React/NextJS 使用 UseEffect 错误:渲染的钩子比上一次渲染期间更多
React - nextjs 在 useEffect 中调用自定义钩子
如何将 React 钩子(useContext、useEffect)与 Apollo 反应钩子(useQuery)结合起来