React避坑指南之useEffect 依賴引用類型問題分析
前言
如果你是一個(gè)入行不久的前端開發(fā),面試中多半會(huì)遇到一個(gè)問題:
你認(rèn)為使用React要注意些什么?
這個(gè)問題意在考察你對(duì)React的使用深度,因?yàn)槌两降貙戇^一個(gè)項(xiàng)目就會(huì)發(fā)現(xiàn),不同于一些替你做決定的框架,“潛規(guī)則”豐富的React遠(yuǎn)比看上去要難相處。
React中主要有兩類坑點(diǎn),一種是讓你措手不及,結(jié)果對(duì)不上預(yù)期,嚴(yán)重影響開發(fā)進(jìn)度,另一種更為頭痛,表面風(fēng)平浪靜,水下暗流涌動(dòng)。
官方文檔的觸角只伸到Demo級(jí)別,并不涉及花樣百出的最差實(shí)踐,所以下一批開發(fā)者又會(huì)掉入相同的陷阱。隱藏的坑點(diǎn)需要開發(fā)者親自下地掃雷,經(jīng)驗(yàn)主義發(fā)揮了重要作用,尤其是在Hooks使用中。
為了避免更多的心智負(fù)擔(dān),這個(gè)系列的文章會(huì)介紹一些React使用的常見陷阱,帶你追溯原因和探索解決方案,幫助新手迅速跳過坑點(diǎn)。
問題提出
const Issue = function () {
const [count, setCount] = useState(0);
const [person, setPerson] = useState({ name: 'Alice', age: 15 });
const [array, setArray] = useState([1, 2, 3]);
useEffect(() => {
console.log('Component re-rendered by count');
}, [count]);
useEffect(() => {
console.log('Component re-rendered by person');
}, [person]);
useEffect(() => {
console.log('Component re-rendered by array');
}, [array]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(1)}>Update Count</button>
<button onClick={() => setPerson({ name: 'Bob', age: 30 })}>Update Person</button>
<button onClick={() => setArray([1, 2, 3, 4])}>Update Array</button>
</div>
);
};在這個(gè)案例中,初始化了三個(gè)狀態(tài),和對(duì)應(yīng)的三個(gè)副作用函數(shù)useEffect,理想狀態(tài)是狀態(tài)的值更新時(shí)才觸發(fā)useEffect。
多次點(diǎn)擊Update Count更新State,因?yàn)楦潞蟮闹颠€是1,所以第一個(gè)useEffect執(zhí)行第一次后不會(huì)重復(fù)執(zhí)行,這符合預(yù)期。但是重復(fù)點(diǎn)擊Update Person和Update Array時(shí),卻不是這樣,盡管值相同,但useEffect每一次都會(huì)觸發(fā)。當(dāng)useEffect中的副作用計(jì)算量較大時(shí),必然會(huì)引起性能問題。
原因追溯
為了追溯這個(gè)原因,可以首先熟悉一下useEffect的源碼:
function useEffect(create, deps) {
const fiber = get();
const { alternate } = fiber;
if (alternate !== null) {
const oldProps = alternate.memoizedProps;
const [oldDeps, hasSameDeps] = areHookInputsEqual(deps, alternate.memoizedDeps);
if (hasSameDeps) {
pushEffect(fiber, oldProps, deps);
return;
}
}
const newEffect = create();
pushEffect(fiber, newEffect, deps);
}
function areHookInputsEqual(nextDeps, prevDeps) {
if (prevDeps === null) {
return false;
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}在上面的代碼中,我們著重關(guān)注areHookInputsEqual的實(shí)現(xiàn),這個(gè)函數(shù)對(duì)比了前后兩次傳入的依賴項(xiàng),決定了后續(xù)副作用函數(shù)create()是否會(huì)執(zhí)行??梢悦黠@看到,useEffect對(duì)于依賴項(xiàng)執(zhí)行的是淺比較,即Object.is (arg1, arg2),這可能是出于性能考慮。對(duì)于原始類型這沒有問題,但對(duì)于引用類型(數(shù)組、對(duì)象、函數(shù)等),這意味著即使內(nèi)部的值保持不變,引用本身也會(huì)發(fā)生變化,導(dǎo)致 useEffect執(zhí)行副作用。
方案探索
1.飲鴆止渴
縫縫補(bǔ)補(bǔ)只是為了等一個(gè)人替你推倒重蓋
最直接的思路是把useEffect的依賴項(xiàng)從引用類型換成基本類型:
useEffect(() => {
console.log('Component re-rendered by person');
}, [JSON.stringify(person)]);
useEffect(() => {
console.log('Component re-rendered by array');
}, [JSON.stringify(array)]);表面上可行,實(shí)際后患無窮(具體參考JSON.stringify為什么不能用來深拷貝),為了避坑而挖另外的坑,顯然不是我們期待的解決方案。
對(duì)比之下,這樣的寫法可以容忍,但是person對(duì)象如果增加了其他屬性,你要確保自己還記得更新依賴,否則依然是掩蓋問題。
useEffect(() => {
console.log('Component re-rendered by person');
}, [person.name, person.age]);2.前置攔截
第二種思路:
在你決定要出手之前,我已經(jīng)幫你決定了 —— 格林公式引申公理
我們可以把問題盡可能前置,手動(dòng)加一層深對(duì)比,如何發(fā)現(xiàn)引用值沒有變化,就不執(zhí)行狀態(tài)更新的邏輯,也就不會(huì)觸發(fā)useEffect重復(fù)執(zhí)行。
<button onClick={() => {
const newPerson = { name: 'Bob', age: 18 };
if (!isEqual(newPerson, person)) {
setPerson(newPerson)}
}
}
>Update person</button>但這樣顯然不太優(yōu)雅,且每一次寫setState時(shí)心智負(fù)擔(dān)太重,對(duì)比邏輯可不可以封裝起來。
3.他山之石
實(shí)際上自定義的Hooks就是為了解決方法級(jí)別的邏輯復(fù)用,這里我們利用useRef綁定的值可以跨渲染周期的特點(diǎn),實(shí)現(xiàn)一個(gè)自定義的useCompare。
const useCompare = (value, compare) => {
const ref = useRef(null);
if (!compare(value, ref.current)) {
ref.current = value;
}
return ref.current;
}經(jīng)過ref記錄的上一次結(jié)果,我們同時(shí)擁有了前后兩次更新的狀態(tài),如果發(fā)現(xiàn)值不同,再讓ref綁定新的引用類型地址。
import { isEqual } from 'lodash';
const comparePerson = useCompare(person, isEqual);
useEffect(() => {
console.log('Component re-rendered by comparePerson');
}, [comparePerson]);
// 重復(fù)執(zhí)行
useEffect(() => {
console.log('Component re-rendered by person');
}, [person]);需要注意的是,這里使用了lodash的isEqual函數(shù)實(shí)現(xiàn)深對(duì)比,看似省心實(shí)際是一個(gè)成本極其不穩(wěn)定的選擇,如果對(duì)象過于龐大,可能得不償失,可以傳入簡(jiǎn)化的compare函數(shù),有取舍的比較常變的key值。
而且每次又到單獨(dú)調(diào)用useCompare生成新的對(duì)象,這里的邏輯也值得被封裝。
4.回歸本質(zhì)
停止曲線救國(guó),直面問題本身。
說了這么多,實(shí)際還是useEffect中對(duì)比邏輯問題,本著支持拓展但不支持修改的原則,我們需要支持一個(gè)新的useEffect支持深度對(duì)比。我們將useRef實(shí)現(xiàn)的記憶引用傳入useEffect的對(duì)比邏輯中:
import { useEffect, useRef } from 'react';
import isEqual from 'lodash.isequal';
const useDeepCompareEffect = (callback, dependencies, compare) => {
// 默認(rèn)的對(duì)比函數(shù)采用lodash.isEqual, 支持自定義
if (!compare) compare = isEqual;
const memoizedDependencies = useRef([]);
if (!compare (memoizedDependencies.current, dependencies)) {
memoizedDependencies.current = dependencies;
}
useEffect(callback, memoizedDependencies.current);
};
export default useDeepCompareEffect;
function App({ data }) {
useDeepCompareEffect(() => {
// 這里的代碼只有在 data 發(fā)生深層級(jí)的改變時(shí)才會(huì)執(zhí)行
console.log('data 發(fā)生了改變', data);
}, [data]);
return <div>Hello World</div>;
}考慮到前文提到的復(fù)雜對(duì)象的深對(duì)比隱患,我依然結(jié)和個(gè)人意志,在useDeepCompareEffect中加了一個(gè)可選參數(shù)compare函數(shù),把isEqual作為一種默認(rèn)模式。于是,我們終于有了一勞永逸的方法。
總結(jié)
實(shí)際上,react-use和a-hooks等第三方庫(kù)都已經(jīng)實(shí)現(xiàn)了useDeepCompareEffect,也可以發(fā)現(xiàn)自定義hooks解決問題將會(huì)是目前體系下一種復(fù)用性極高的實(shí)踐。通過這些方法的推導(dǎo),也可以看出我們獲取方案的思路,希望對(duì)新手的成長(zhǎng)有所幫助。
到此這篇關(guān)于React避坑指南之useEffect 依賴引用類型問題分析的文章就介紹到這了,更多相關(guān)React useEffect 依賴引用類型內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React?Router?v6路由懶加載的2種方式小結(jié)
React?Router?v6?的路由懶加載有2種實(shí)現(xiàn)方式,1是使用react-router自帶的?route.lazy,2是使用React自帶的?React.lazy,下面我們就來看看它們的具體實(shí)現(xiàn)方法吧2024-04-04
React項(xiàng)目使用ES6解決方案及JSX使用示例詳解
這篇文章主要為大家介紹了React項(xiàng)目使用ES6解決方案及JSX使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
react實(shí)現(xiàn)Radio組件的示例代碼
這篇文章主要介紹了react實(shí)現(xiàn)Radio組件的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04
解析TypeError:import_react_native.AppState.removeEventListener
這篇文章主要為大家介紹了TypeError:import_react_native.AppState.removeEventListener?is?not?a?function問題解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
自己動(dòng)手封裝一個(gè)React Native多級(jí)聯(lián)動(dòng)
這篇文章主要介紹了自己動(dòng)手封裝一個(gè)React Native多級(jí)聯(lián)動(dòng),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-09-09

