什么时候执行清除函数
我们知道,如果在 useEffect 函数中返回一个函数,这个函数就是清除副作用函数,它会在组件销毁的时候执行。但是其实,它会在组件每次重新渲染时执行,并且先执行清除上一个 effect 的副作用。即:
- 首次渲染不会进行清理,会在下一次渲染,清除上一次的副作用。
- 卸载阶段也会执行清除操作。
以下面计数器为例:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c=>c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
// state count=0
const id = setInterval(() => {setCount(0=>0 + 1);}, 1000); // 运行第一个 effect
// state count=1
clearInterval(id); // 清除上一个 effect
const id = setInterval(() => {setCount(1=>1 + 1);}, 1000); // 运行下一个 effect
// state count=2
clearInterval(id); // 清除上一个 effect
const id = setInterval(() => {setCount(2=>2 + 1);}, 1000); // 运行下一个 effect
// 组件卸载
clearInterval(id); // 清除最后一个 effect
为什么有时候在useEffect里拿到的props或state是旧的?
useEffect 拿到的总是定义它的那次渲染中的 props 和 state。当useEffect的第二个参数传递 [] 数组的时候,拿到的是旧的值。无依赖项时只在组件挂载时运行一次,并非重新渲染。
1. 不要对useEffect的依赖撒谎
如果你明明使用了某个变量,却没有申明在依赖中,你等于向 React 撒了谎,后果就是,当依赖的变量改变时,useEffect 也不会再次执行:
useEffect(() => {
document.title = "Hello, " + name;
}, []);// 这个 effect 依赖于 `name` state
看下面的例子:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 这个 effect 依赖于 `count` state }, 1000);
return () => clearInterval(id);
}, []); // 🔴 Bug: `count` 没有被指定为依赖
return <h1>{count}</h1>;
}
setInterval 只想执行一次,所以自以为聪明的向 React 撒了谎,将依赖写成 []。
组件初始化执行一次 setInterval,销毁时执行一次 clearInterval,这样的代码以为符合预期,拿到的 count 值永远是初始化的 0。
在初始渲染时,执行effect,这里会创建一个闭包,并将 count 的值被保存在该闭包当中,且初值为 0。每隔一秒,回调就会执行 setCount(0 + 1),因此,count 永远不会超过 1。
2. 诚实的代价
将count加入依赖列表就能修复这个bug。
因为useEffect的清除函数式在每次渲染后都会执行,所以每次count发生改变定时器都被重置。1. 频繁 生成/销毁 定时器带来了一定性能负担。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
return <h1>{count}</h1>;
}
3. 既诚实又高效
(1)使用setState的函数式更新形式
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
(2)使用ref来保存可变的变量
useRef不仅可以用于 DOM refs,ref 对象是一个 current 属性可变且可以保存任意可变值。(useRef 会在每次渲染时返回同一个 ref 对象,即在组件的整个生命周期内保持不变,变更 .current 属性不会引发组件重新渲染。)
const [count, setCount] = useState(0);
const latestCount = useRef();
useEffect(()=>{
latestCount.current = count
},[count])
useEffect(() => {
const id = setInterval(() => {
setCount(latestCount.current + 1);
}, 1000);
return () => clearInterval(id);
}, []);
(3)将更新与动作解耦
在一些更加复杂的场景中比如同时依赖了两个state的情况:
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
这时可以用useReducer把 state 更新逻辑移到 useEffect 之外,React会保证dispatch在每次渲染中都是一样的。
const initialState = {
count: 0,
step: 1,
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === 'tick') {
return { count: count + step, step };
} else if (action.type === 'step') {
return { count, step: action.step };
} else {
throw new Error();
}
}
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' }); // 代替 setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, []);
为什么会出现重复请求的问题?
- 通常是没有设置 effect 依赖参数(没有设置依赖,effect会在每次渲染后执行一次),但在effect 里做数据请求并更新状态,引起渲染再次出发effect。
- 也可能是我们设置的依赖总在改变,如果依赖是个函数(没有用useCallback或者useMemo包一层),在组件内定义的函数每一次渲染都在变。
useEffect 不支持 async function
useEffect(async () => {
await getPoiInfo(); // 请求数据
}, []);
上面的代码是很常见的需求,但是react并不支持这么做。
effect function 应该返回一个销毁函数(是指return返回的cleanup函数),如果 useEffect 第一个参数传入 async,返回值则变成了 Promise,会导致 react 在调用销毁函数的时候报错。
第二次渲染触发 useEffect 里的回调前,前一次渲染触发的行为都执行完成,返回的清理函数也执行完成。这样逻辑才清楚。而如果是异步的,情况会变得很复杂,可能会很容易写出有 bug 的代码。所以 React 就直接限制了不能 useEffect 回调函数中不能支持 async...await...
useEffect 怎么支持 async...await...?
1. 简单改造
创建一个异步函数(async...await 的方式),然后执行该函数。
useEffect(() => {
const asyncFun = async () => {
const count = await fetchData({type,});
setCount(count);
};
asyncFun();
}, [type]);
复制代码
也可以使用 IIFE,如下所示:
useEffect(() => {
(async () => {
const count = await fetchData({type,});
setCount(count);
})();
}, [type]);
2. 自定义hooks
自定义 hook包裹,然后再effect中通过promise.then调用
// 自定义hook
function useAsyncEffect(effect: () => Promise<void | (() => void)>, dependencies?: any[]) {
return useEffect(() => {
const cleanupPromise = effect()
return () => { cleanupPromise.then(cleanup => cleanup && cleanup()) }
}, dependencies)
}
// 使用
useAsyncEffect(async () => {
const count = await fetchData()
setCount(count)
}, [fetchData])
或者利用useCallback/useMemo
// 封装
const requestData = useCallback(async () => {
const count = await fetchData({type,})
setCount(count)
}, [type]); // type改变会重新生成函数
// 普通接口请求
useEffect(() => {
requestData();
}, [requestData]);
高阶版可以参考ahooks库 的 useAsyncEffect