React深入useEffect
本文適合熟悉React、以及在用useEffect遇到難題的小伙伴進(jìn)行閱讀。
歡迎關(guān)注前端早茶,與廣東靚仔攜手共同進(jìn)階~
作者:廣東靚仔
一、前言
本文基于開源項目:
https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js
// 彈框顯示觸發(fā)定時器
useEffect(() => {
timer = setInterval(() => {
if (showModal) {
requestFun()
}
}, 1000)
}, [showModal])
// 關(guān)閉彈框,清除定時器
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的函數(shù)會在組件渲染到屏幕之后執(zhí)行。useEffect一般是在每輪渲染結(jié)束后執(zhí)行,當(dāng)然我們也可以讓它在只有某些值改變的時候才執(zhí)行。useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除訂閱
subscription.unsubscribe();
};
});
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
三、useEffect原理
useEffect實際上是ReactCurrentDispatcher.current.useEffect(源碼解析會講到)
useEffect原理可以簡單理解為:
函數(shù)組件在掛載階段會執(zhí)行MountEffect,維護(hù)hook的鏈表,同時專門維護(hù)一個effect的鏈表。 在組件更新階段,會執(zhí)行UpdateEffect,判斷deps有沒有更新,如果依賴項更新了,就執(zhí)行useEffect里操作,沒有就給這個effect標(biāo)記一下NoHookEffect,跳過執(zhí)行,去下一個useEffect。
我們都知道useEffect 在依賴變化時,執(zhí)行回調(diào)函數(shù)。這個變化是指本次 render 和上次 render 時的依賴之間的比較。
默認(rèn)情況下,effect 會在每輪組件渲染完成后執(zhí)行,而且effect 觸發(fā)后會把清除函數(shù)暫存起來,等下一次 effect 觸發(fā)時執(zhí)行,大概過程如下:

溫馨提示:使用 hooks 要避免 if、for 等的嵌套使用
四、useEffrct源碼解析
在react源碼中,我們找到react.js中如下代碼,篇幅有限,廣東靚仔進(jìn)行了簡化,方便小伙伴閱讀:
4.1 useEffect引入與導(dǎo)出
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);
}上面的代碼就是引入與導(dǎo)出過程,不難看出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 組件加載調(diào)用mountEffect
函數(shù)組件加載時,useEffect會調(diào)用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是二進(jìn)制常數(shù),用位運算的方式操作,用來標(biāo)記是什么類型的副作用的。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 {
// 維護(hù)了一個副作用的鏈表,還是環(huán)形鏈表
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鏈表中,并且單獨維護(hù)了一個useEffect的鏈表。
4.3 組件更新時調(diào)用updateEffect
函數(shù)組件加載時,useEffect會調(diào)用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 {
// 獲取當(dāng)前正在工作的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;
// 比較依賴項是否發(fā)生變化
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果兩次依賴項相同,componentUpdateQueue增加一個tag為NoHookEffect = 0 的effect,
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 兩次依賴項不同,componentUpdateQueue上增加一個effect,并且更新當(dāng)前hook的memoizedState值
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
從上面代碼中我們看到areHookInputsEqual用來比較依賴項是否發(fā)生變化。下面我們看看這個areHookInputsEqual函數(shù)
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數(shù)組,對每一項執(zhí)行Object.is()方法,判斷兩個值是否為同一個值。
以上內(nèi)容是源碼中的一部分,如果感興趣的小伙伴可以到react倉庫進(jìn)行閱讀~
五、總結(jié)
在看源碼前,我們先去官方文檔復(fù)習(xí)下框架設(shè)計理念、源碼分層設(shè)計 閱讀下框架官方開發(fā)人員寫的相關(guān)文章 借助框架的調(diào)用棧來進(jìn)行源碼的閱讀,通過這個執(zhí)行流程,我們就完整的對源碼進(jìn)行了一個初步的了解 接下來再對源碼執(zhí)行過程中涉及的所有函數(shù)邏輯梳理一遍
關(guān)注我,一起攜手進(jìn)階
如果這篇文章有觸動到你,歡迎關(guān)注前端早茶,與廣東靚仔攜手共同進(jìn)階~
