本文適合熟悉React、以及在用useEffect遇到難題的小夥伴進行閱讀。
歡迎關注前端早茶,與廣東靚仔攜手共同進階~
作者:廣東靚仔
一、前言本文基於開源項目:
https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js
今年廣東靚仔報名了軟考,業餘把精力更多投入到複習中。由於疫情影響,今天廣州區暫停了軟考上半年的相關科目,廣東靚仔又來寫文章了。 廣東靚仔將從三個方面來梳理useEffect相關內容:相信有不少小夥伴在使用useEffect過程中遇到過不少問題,廣東靚仔找來了幾個有bug的例子:// 彈框顯示觸發定時器
useEffect(() => {
timer = setInterval(() => {
if (showModal) {
requestFun()
}
}, 1000)
}, [showModal])
// 關閉彈框,清除定時器
const closeModal = () => {
clearInterval(timer)
}useEffect(() => {
let intervalId = setInterval(() => {
fetchData();
}, 1000 * 60);
return () => {
clearInterval(intervalId);
intervalId = null;
}
}, [])
const fetchData = () => {
request({params}).then(ret => {
if (ret.code === OK) {
applyResult(ret.data);
}
})
}
當我們在useEffect調用第三方庫的實例,然後在其他函數清除這個實例,發現無法清除。其他的小夥伴在用useEffect還遇到過其他的問題,這裡就不一一展開,閱讀完這篇文章後,一定會對useEffect有一個全面的理解。二、useEffect介紹 React16.8版本中描述了在 React 渲染階段,改變 DOM、添加訂閱、設置定時器、記錄日誌以及執行其他包含副作用的操作是不被允許的,因為可能會產生莫名其妙的 bug 並破壞 UI 的一致性。 因此在使用useEffect完成副作用操作,賦值給useEffect的函數會在組件渲染到屏幕之後執行。useEffect一般是在每輪渲染結束後執行,當然我們也可以讓它在只有某些值改變的時候才執行。useEffect有個清除函數,官方demo如下:useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除訂閱
subscription.unsubscribe();
};
});
一般在執行一些計時器或者訂閱,我們會在組件卸載後,會清除這些內容。因此可以在清除函數裡面做這些操作。
useEffect為防止內存洩漏,一般情況下如果組件多次渲染,在執行下一個effect 之前,上一個 effect 就已被清除。也就是說組件的每一次更新都會創建新的訂閱。useEffect 的函數會在瀏覽器完成布局與繪製之後,在一個延遲事件中被調用。我們都知道一旦 effect 的依賴發生變化,它就會被重新創建,例如:useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
useEffect傳遞第二個參數,它是 effect 所依賴的值數組。只有當依賴改變後才會重新創建訂閱。溫馨提示:有很多小夥伴在日常項目開發的時候,使用這個依賴的時候,很容易留下bug。比如:一個編輯彈框功能,如果useEffect依賴只寫了個id,這個時候如果是對同一條數據進行編輯是不會再次執行useEffect的邏輯的。三、useEffect原理useEffect實際上是ReactCurrentDispatcher.current.useEffect(源碼解析會講到)
useEffect原理可以簡單理解為:
函數組件在掛載階段會執行MountEffect,維護hook的鍊表,同時專門維護一個effect的鍊表。在組件更新階段,會執行UpdateEffect,判斷deps有沒有更新,如果依賴項更新了,就執行useEffect裡操作,沒有就給這個effect標記一下NoHookEffect,跳過執行,去下一個useEffect。我們都知道useEffect 在依賴變化時,執行回調函數。這個變化是指本次 render 和上次 render 時的依賴之間的比較。
默認情況下,effect 會在每輪組件渲染完成後執行,而且effect 觸發後會把清除函數暫存起來,等下一次 effect 觸發時執行,大概過程如下:
溫馨提示:使用 hooks 要避免 if、for 等的嵌套使用
四、useEffrct源碼解析在react源碼中,我們找到react.js中如下代碼,篇幅有限,廣東靚仔進行了簡化,方便小夥伴閱讀:
4.1 useEffect引入與導出
import {
...
useEffect,
...
} from './ReactHooks';// ReactHooks.js
export function useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
if (__DEV__) {
if (dispatcher === null) {
// React版本不對或者Hook使用有誤什麼的就報錯...
}
}
return ((dispatcher: any): Dispatcher);
}上面的代碼就是引入與導出過程,不難看出useEffect實際上是ReactCurrentDispatcher.current.useEffect橙色的代碼。
import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;current的類型是null或者Dispatcher,不難看出接下來我們要找類型定義
// ReactInternalTypes.js
export type Dispatcher = {|
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
|};4.2 組件加載調用mountEffect
函數組件加載時,useEffect會調用mountEffect,接下來我們來看看mountEffect
// ReactFiberHooks.new.js
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
}PassiveEffect和PassiveStaticEffect是二進位常數,用位運算的方式操作,用來標記是什麼類型的副作用的。mountEffect走了mountEffectImpl方法
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}上面代碼中,往hook鍊表裡追加一個hook,把hook存到鍊表中以後還把pushEffect的返回值存了下來。
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy, // mountEffectImpl傳過來的是undefined
deps,
next: (null: any),
};
// 一個全局變量,在renderWithHooks裡初始化一下,存儲全局最新的副作用
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 維護了一個副作用的鍊表,還是環形鍊表
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 最後一個副作用的next指針指向了自身
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}最後返回了一個effect對象。
Tips: mountEffect就是把useEffect加入了hook鍊表中,並且單獨維護了一個useEffect的鍊表。
4.3 組件更新時調用updateEffect
函數組件加載時,useEffect會調用updateEffect,接下來我們來看看updateEffect
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 獲取當前正在工作的hook
const hook = updateWorkInProgressHook();
// 最新的依賴項
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
// 上一次的hook的effect
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 比較依賴項是否發生變化
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果兩次依賴項相同,componentUpdateQueue增加一個tag為NoHookEffect = 0 的effect,
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 兩次依賴項不同,componentUpdateQueue上增加一個effect,並且更新當前hook的memoizedState值
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}從上面代碼中我們看到areHookInputsEqual用來比較依賴項是否發生變化。下面我們看看這個areHookInputsEqual函數
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) {
...
return false;
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}上面代碼中,廣東靚仔刪掉了一些dev處理的代碼,不影響閱讀。
其實就是遍歷deps數組,對每一項執行Object.is()方法,判斷兩個值是否為同一個值。
以上內容是源碼中的一部分,如果感興趣的小夥伴可以到react倉庫進行閱讀~
五、總結 在我們閱讀完官方文檔後,我們一定會進行更深層次的學習,比如看下框架底層是如何運行的,以及源碼的閱讀。在看源碼前,我們先去官方文檔複習下框架設計理念、源碼分層設計藉助框架的調用棧來進行源碼的閱讀,通過這個執行流程,我們就完整的對源碼進行了一個初步的了解接下來再對源碼執行過程中涉及的所有函數邏輯梳理一遍關注我,一起攜手進階
如果這篇文章有觸動到你,歡迎關注前端早茶,與廣東靚仔攜手共同進階~