根据反应文档,useEffect
将在重新运行useEffect
部分之前触发清理逻辑。
如果您的效果返回了一个函数,React将在需要清理时运行它...
由于
useEffect
默认处理更新,因此没有用于处理更新的特殊代码。在应用下一个效果之前,它将清理以前的效果...
但是,当我在requestAnimationFrame
中使用cancelAnimationFrame
和useEffect
时,我发现cancelAnimationFrame可能无法正常停止动画。有时,我发现旧动画仍然存在,而下一个效果带来了另一个动画,这导致了我的Web应用程序性能问题(尤其是当我需要渲染沉重的DOM元素时)。
我不知道react钩子是否会在执行清理代码之前做一些额外的事情,这会使我的cancel-animation部分无法正常工作,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>
答案 0 :(得分:3)
在使用钩子并尝试实现生命周期功能时,需要着眼于两种不同的钩子。
根据文档:
useEffect
在react渲染您的组件并确保 您的效果回调不会阻止浏览器绘画。这不同 来自类组件中的行为,其中componentDidMount
和componentDidUpdate
在渲染后同步运行。
,因此在这些生命周期中使用requestAnimationFrame
似乎没有效果,但useEffect
却有一些小故障。因此,当您必须进行的更改不会阻止视觉更新(例如在收到响应后进行导致DOM更改的API调用)时,应该使用useEffect。
useLayoutEffect
是另一个不那么流行但是在处理可视DOM更新时非常方便的钩子。 As per the docs
签名与useEffect相同,但是它同步触发 在所有DOM突变之后。使用它从DOM中读取布局,并 同步重新渲染。在
useLayoutEffect
内部安排的更新将 在浏览器有机会绘画之前被同步刷新。
因此,如果您的效果是通过DOM节点ref对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>
答案 1 :(得分: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 <></>
}
答案 2 :(得分: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')
})