一、Effect Hook 是啥?
Hook 是以 use
开头的特殊函数,让 函数组件 拥有 calss组件 的某些特性。Effect Hook 就是指 useEffect
这个特殊函数,它让 函数组件 能在渲染完成后执行自定义操作。
useEffect
中要谨慎使用 useState
,因为它会触发组件渲染后,再次调用 useEffect,形成一个死循环。正确方式:用条件语句包裹 useState 方法,定义了退出条件,避开死循环。
二、3种使用方式
让组件只监控特定数据的变更,再进行渲染后的操作,忽略不必要的操作,很好的优化了组件性能。
1、useEffect(() => { })
只有一个参数,每一次组件渲染完成后 且 在下一次渲染前 被调用。
// 1、导入useEffect;
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 2、调用,箭头函数作为其唯一参数
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
2、useEffect(() => { }, [])
有两个参数,第二个参数是空数组([]
) 。在组件首次加载渲染完成后被调用,且只被调用这一次。
useEffect(() => {
function doSomething() {
console.log('hello');
}
doSomething();
}, []);
3、useEffect(() => { }, [count])
有两个参数,第二个参数是数组。只有当数组里面的值改变时,useEffect
才会被调用。
// 只有当 `props.source` 改变后才会调用 useEffect。
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
三、清除机制
useEffect
被调用时,执行了一些操作(譬如设定了一个定时器、访问了一些网络资源),在组件卸载时,必须做一些清除操作来防止内存泄露等问题。
解决方案:只需要在 useEffect
中返回一个清除函数,React会在组件卸载之前调用清除函数。
// 函数组件中实现:用户登录状态更新和清除
// ChatAPI是假设的模块,它允许我们订阅好友的在线状态。
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// 返回清除函数
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
四、依赖值频繁变化,怎么办?
1、问题:
useEffect 没有指定依赖,意味着 useEffect 只会运行一次,其内部获取到的 count 永远是初始值0,导致页面 中的<h1>{count}</h1>
值,永远是1。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
2、不完美解决方案
在 useEffect 中添加 count 依赖,这样每一次 useEffect 执行 setCount 带来count的变化,都会使得 useEffect 再次被调用,可以解决问题,但是这样会带来另一个问题,每一次执行 useEffect 都会清除计时器,再重新设置计时器,这不是我们想要的。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 这个 effect 依赖于 `count` state
}, 1000);
return () => clearInterval(id);
}, [count]); // 添加 count 依赖
return <h1>{count}</h1>;
}
3、最终解决方案
采用 setState 的更新特性,让 setCount 自己去获取和更新 count,让 useEffect 完全脱离对 count 的依赖,实现最终理想效果。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
五、函数组件中实现class组件中的this
1、useRef + useEffect
使用 useRef
和 useEffect
来实现,仅当你实在找不到更好办法的时候才这么做,因为依赖于变更会使得组件更难以预测。
function Example(props) {
// 把最新的 props 保存在一个 ref 中
const latestProps = useRef(props);
useEffect(() => {
latestProps.current = props;
});
useEffect(() => {
function tick() {
// 在任何时候读取 props 都是最新的
console.log(latestProps.current);
}
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []); // 这个 effect 只会执行一次
}
六、依赖回调函数更新
1、useCallback + useEffect
function ProductPage({ productId }) {
// ✅ 用 useCallback 包裹以避免随渲染发生改变
const fetchProduct = useCallback(() => {
// ... Does something with productId ...
}, [productId]); // ✅ useCallback 的所有依赖都被指定了
return <ProductDetails fetchProduct={fetchProduct} />;
}
function ProductDetails({ fetchProduct }) {
useEffect(() => {
fetchProduct();
}, [fetchProduct]); // ✅ useEffect 的所有依赖都被指定了
// ...
}
七、useLayoutEffect
useEffect
会在组件完全渲染完后被调用执行,此时执行的 useEffect ,如果涉及到可见DOM变更,就可能给客户带来视觉上的跳跃感,此时可以考虑使用 useLayoutEffect
,使用方式跟 useEffect
完全一样,只是被调用的时机不同。