原因
最近还是发现自己在使用useEffect时有疑惑,深感有必要进一步去学习useEffect的使用。
疑惑一
const [variable,setVariable] = useState('');
// 这样写有必要吗?
useEffect(_ =>{},[])
- 没必要,没意义
疑惑二: 依赖数组到底该写些啥
概念:
- 依赖数组:用来告诉 React 这个 useEffect 何时应该运行。依赖数组中的每一项会被监控,当这些值发生变化时,副作用函数会重新运行。
- 依赖数组中的典型值
- 状态(state):如果副作用依赖于某个 useState 的值,应该将其包含在依赖数组中。
- props:当副作用依赖于某个 props,应该将其包含在依赖数组中。
- 函数或其他变量:如果副作用依赖于某个函数或者计算出来的变量,也应该包括在依赖数组中。
- 依赖数组中尽量不要有
object
或array
等引用变量
错误示范:
function ChatRoom({ articleId }) {
const [article, setArticle] = useState(null);
// options 會在每次渲染時重新創建
const options = {
serverUrl: 'https://localhost:1234',
articleId: articleId
};
useEffect(() => {
const data = getArticle(options);
setArticle(data)
// options 每次渲染時值都不同,因此觸發 useEffect 執行
}, [options]);
}
正确示范:
function ChatRoom({ articleId }) {
const [article, setArticle] = useState(null);
useEffect(() => {
const options = {
serverUrl: 'https://localhost:1234',
articleId: articleId
};
const data = getArticle(options);
setArticle(data)
}, [articleId]);
}
参考
- Stop Lying to React About Missing Dependencies | by Jack Taylor | Better Programming
- “React中useEffect的依赖数组:空数组与不写数组的区别”- 掘金
疑惑三: 依赖数组为空和不传有区别吗?
概念
- 当依赖数组为空时(
[]
),useEffect中的回调函数只会在组件挂载和卸载时执行。这意味着它只会执行一次,类似于类组件中的componentDidMount
和componentWillUnmount
。 - 当不传入依赖数组时,useEffect中的回调函数会在每次组件渲染时都执行。这意味着它会在组件挂载、更新和卸载时都执行。
示例: 依赖数组为空
//10秒内连续点击按钮5次,控制台的输出结果是什么
const [count, setCount] = useState(0)
useEffect(() => {
setTimeout(() => {
console.log(count)
}, 10 * 1000);
},[])
//控制台只会输出一个0
//因为count更新只是组件状态更新,而此时的useeffect只在组件加载和卸载时候执行只执行一次
<button onClick={() =>{setCount(count+1)}}>点击count加1</button>
示例: 不传入依赖数组
//10秒内连续点击按钮5次,控制台的输出结果是什么
const [count, setCount] = useState(0)
useEffect(() => {
setTimeout(() => {
console.log(count)
}, 10 * 1000);
})
//控制台只会输出0,1,2,3,4,5
//因为count更新,组件状态更新会重新渲染组件
//而此时的useeffect在组件更新渲染的时候都会执行
//所以会执行对应的点击次数
//在控制台中打印对应的次数
<button onClick={() =>{setCount(count+1)}}>点击count加1</button>
参考
疑惑四: 依赖数组是怎么比较的
概念
- 使用
Object.is
来进行比较,如果依赖数组中任意一值与前一次不用,就会重新执行 Object.is
和===
的唯一区别在于它们处理带符号的 0 和 NaN 值的时候。=== 运算符(和 == 运算符)将数值 -0 和 +0 视为相等,但是会将 NaN 视为彼此不相等。===
不执行类型转换。它将验证被比较的变量是否具有相同的值和相同的类型。
实现useDeepCompareEffect
- 先用useRef取得前一次的值,然后再调用深比较函数对比前后的值
- 代码
import { useRef } from 'react'; import type { DependencyList, useEffect, useLayoutEffect } from 'react'; import isEqual from 'lodash/isEqual'; type EffectHookType = typeof useEffect | typeof useLayoutEffect; type CreateUpdateEffect = (hook: EffectHookType) => EffectHookType; const depsEqual = (aDeps: DependencyList = [], bDeps: DependencyList = []) => { return isEqual(aDeps, bDeps); }; export const createDeepCompareEffect: CreateUpdateEffect = (hook) => (effect, deps) => { const ref = useRef<DependencyList>(); const signalRef = useRef<number>(0); // 本地更新的依赖值与缓存的依赖深比较 if (deps === undefined || !depsEqual(deps, ref.current)) { // 将依赖保存一份 ref.current = deps; // 如果发现变更,则改变signalRef的值,是为了触发真正的useEffect signalRef.current += 1; } hook(effect, [signalRef.current]); };
报错:useEffect
is called conditionally
完整报错如下:
React Hook “useEffect” is called conditionally. React Hooks must be called in the exact same order in every component render. react-hooks/rules-of-hooks
解决方法很简单,只需将useEffect调用移动到组件的顶层,并使用条件语句来决定是否执行其内部的逻辑即可。
useEffect 执行时机
- 當元件被加入時 (mount),useEffect 會被第一次執行。
- 當每次元件重新渲染時,如果 dependencies 的值有改變,先將舊的 props 和 state 執行 cleanup function,再帶著新的 props 和 state 執行 setup function。
- cleanup function 的程式碼,會在元件生命週期結束 (unmount) 時,執行最後一次。
useLayoutEffect
概念
- 执行时机: 在浏览器重绘 (repaints) 前執行,区别于
useEffect
,在浏览器完成布局与绘制之后,在一个延迟事件中被调用 - 可能會造成性能的問題。 因為在 useLayoutEffect 里的程序會阻礙瀏覽器重繪 (repaints) ,太頻繁使用可能會造成整個應用程式緩慢
示例
- 代码
import { useEffect, useLayoutEffect, useState } from "react"; export default function App() { const [count, setCount] = useState(0); useEffect(() => { if (count === 0) { const randomNum = 1 + Math.random() * 1000; setCount(randomNum); } }, [count]); return <div onClick={() => setCount(0)}>{count}</div>; }
- 解释
- 原因是,當你每次點擊 div,此時 count 會被更新為 0,畫面會重新渲染變為 0,同時,因為 count 被更新,也會觸發 useEffect 執行。所以在重繪完成之後, useEffect 執行並把 count 更新為另一串隨機數字,畫面也會再渲染一次,因為兩次渲染時間很快,所以造成閃爍。
- 假設我們把上方程式碼的 useEffect 換成 useLayoutEffect,當你每次點擊 div,此時 count 會被更新為 0,但這時,畫面不會被重新渲染變為 0,而是先等待 useLayoutEffect 內的程式碼執行完畢之後,state 已經更新為新的隨機數字,這時畫面才進行重繪。
使用场景
設計一個工具提示元件 (tooltip),它會依照不同條件出現在某元素的上方、下方或旁邊,這種設計條件代表說,我們會需要知道此元素準確的高度位置,才能判斷要將 tooltip 顯示在哪裡。
此时React的渲染步驟可以拆分為下
- 在任何地方渲染 Tooltip (即使位置不正確)
- 測量元素的高度,並決定放置 Tooltip 的位置
- 重新渲染畫面,這時 Tooltip 的位置才會是正確的
- 如果是使用 useEffect 的話,Tooltip 的位置,可能是從 0 變到 10 的位置,這會造成畫面閃爍、使用者體驗不佳,如果是使用 useLayoutEffect,React 則會在重繪前,就重新計算正確的位置,才渲染畫面。