数据获取,设置订阅以及手动更改React组件中的DOM都属于副作用。
componentDidMount:组件加载; componentDidUpdate:组件更新;
在React Class中,把副作用操作放到componentDidMount和componentDidUpdate中。
useEffect:componentDidMount、componentDidUpdate和componentWillUnmount三个函数的组合; 通过这个hook,可以告诉React组件需要在渲染后执行某些操作。 useEffect会在每次渲染后都执行吗?是的。默认情况下,它会在第一次渲染之后和每次更新之后都会执行。React保证每次运行effect的同时,DOM都已经更新完毕。
【与componentDidMount和componentDidUpdate不同,使用useEffect调度的effect不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect不需要同步地执行。在个别情况下(例如测量布局),有单独的useLayoutEffect Hook,其API与useEffect相同】
需要清除的effect,如订阅外部数据源。 这种情况下,清除工作是非常重要的,可以防止内存泄漏。
React class中,通常会在componentDidMount中设置订阅,并在componentWillUnmount中清除它。 `
componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); }
` 使用Hook的示例 如果你的effect返回一个函数(可选),React将会在执行清除操作时调用它:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline)
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect
// 原本这个cleanup函数是放在componentWillUnmount中
return function cleanup() {
ChatAPI.unsubscribeToFriendStatus(props.friend.id, handleStatusChange);
}
})
React何时清除effect?React会在组件卸载的时候执行清除操作。 并不是必须为 effect 中返回的函数命名。但其实也可以返回一个箭头函数或者给起一个别的名字。
解释: 为什么每次更新的时候都要运行 Effect
为什么 effect 的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次? 忘记正确地处理componentDidUpdate是React中常见的bug来源,useEffect避免了在class组件中因为没有处理更新逻辑而导致常见的bug。 (为了处理React组件更新阶段可能导致的bug。。。
通过跳过Effect进行性能优化:
在某些情况下,每次渲染后都执行清理或者执行effect可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
。。。 对于有清除操作的effect同样适用:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。
如何处理函数以及数组频繁变化时的措施? 在依赖列表中省略函数是否安全?
// 不安全
function Example({ someProp }) {
function doSomething() {
console.log(someProp);
}
useEffect(() => {
doSomething()
}, []); // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)
}
要记住effect外部的函数使用了哪些props和state很难,通常会在effect内部去声明它所需要的函数。
function Example({ someProp }) {
useEffect(() => {
function doSomething() {
console.log(someProp); }
doSomething();
}, [someProp]); // ✅ 安全(我们的 effect 仅用到了 `someProp`)}
如果你指定了一个 依赖列表 作为 useEffect、useLayoutEffect、useMemo、useCallback 或 useImperativeHandle 的最后一个参数,它必须包含回调中的所有值,并参与 React 数据流。这就包括 props、state,以及任何由它们衍生而来的东西。
这同时也允许你通过 effect 内部的局部变量来处理无序的响应:
useEffect(() => {
let ignore = false;
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId)
const json = await response.json()
if(!ignore) setProduct(json)
}
fetchProduct()
return () => { ignore = true }
}, [productId])
如果出于某些原因你 无法 把一个函数移动到 effect 内部,还有一些其他办法: react.docschina.org/docs/hooks-…
- 把函数加入 effect 的依赖但 把它的定义包裹 进 [
useCallback]万不得已的情况下,你可以 把函数加入 effect 的依赖但 把它的定义包裹 进useCallbackHook。
function ProductPage({ productId }) {
// ✅ 用 useCallback 包裹以避免随渲染发生改变
const fetchProduct = useCallback(() => {
// ... Does something with productId ...
}, [productId]); // ✅ useCallback 的所有依赖都被指定了
return <ProductDetails fetchProduct={fetchProduct} />
}
function ProductDetails({ fetchProduct }) {
useEffect(() => {
fetchProduct();
}, [fetchProduct]); // ✅ useEffect 的所有依赖都被指定了
// ...
}
注意在上面的案例中,我们 需要 让函数出现在依赖列表中。这确保了 ProductPage 的 productId prop 的变化会自动触发 ProductDetails 的重新获取。