作用
副作用
前端開發中的副作用一般有:dom操作、瀏覽器事件的綁定和取消綁定、http請求、打印日誌、訪問系統狀態、執行IO更新等。
在class類組件中,副作用一般寫在componentDidMount,componentDidUpdate, componentWillUnMount, componentWillUpdate裏,但是函數組件沒有生命週期,這個時候就可以用useEffect來解決了,這一個hook可以替代以上四個生命週期;
useEffect的使用
useEffect是組件第一次渲染和每次更新,即componentDidMount,componentDidUpdate時都會執行的;
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
create方法是每次執行useEffect時都會執行一次
create方法可以返回一個清除方法(只能是一個普通的方法),該方法會在componentWillUnMount,componentWillUpdate時執行;
一般在這個時候清除副作用;
如果不需要清除的話,不寫返回清除方法即可。
deps:依賴項組成的數組,這個參數可以控制useEffect的方法不要每次執行,只有數組裏的依賴的值發生改變時再執行。
如果useEffect只需要在第一次渲染時執行一次,deps傳入空數組即可;
如果deps不傳,則組件每次更新時都會執行useEffect裏的方法;
源碼分析
由上一篇的useState和useReducer可知,useEffect也是根據組件第一次渲染和更新分別調用的不同的方法,他們分別是:mountEffect和updateEffect。
- 先來看一下mountEffect,第一次渲染組件時做了什麼
// ReactFiberHooks.js
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
UpdateEffect | PassiveEffect,
UnmountPassive | MountPassive,
create,
deps,
);
}
由源碼可知,mountEffect
走了mountEffectImpl
方法,並傳入了個奇奇怪怪得東西UpdateEffect | PassiveEffect
和UnmountPassive | MountPassive
-
UpdateEffect | PassiveEffect
和UnmountPassive | MountPassive
是什麼?
答: 是一個二進制數字,用來標記是什麼類型的副作用的 -
繼續看mountEffectImpl方法
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
//往hook鏈表裏追加一個hook
const hook = mountWorkInProgressHook();
// 沒有傳deps時,會被處理成null
const nextDeps = deps === undefined ? null : deps;
sideEffectTag |= fiberEffectTag;
hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
useEffect的hook的memoizedState對象好像不太一樣,把hook存到鏈表中以後還把pushEffect的返回值存了下來。
pushEffect
做了什麼呢,很重要的亞子!
// 返回了一個effect對象
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy, // mountEffectImpl傳過來的是undefined
deps,
// Circular
next: (null: any),
};
// 一個全局變量,在renderWithHooks裏初始化一下,存儲全局最新的副作用
if (componentUpdateQueue === null) {
// { lastEffect: null }
componentUpdateQueue = createFunctionComponentUpdateQueue();
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;
}
所以mountEffect就是把useEffect加入了hook鏈表中,並且單獨維護了一個useEffect的鏈表。
- 再來看看組件更新時調用updateEffect做了什麼
// 跟mountEffect的執行類似,只不過走的是updateEffectImpl方法
// 直接看updateEffectImpl
function updateEffectImpl(fiberEffectTag, hookEffectTag, 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,
pushEffect(NoHookEffect, create, destroy, nextDeps);
return;
}
}
}
// 兩次依賴項不同,componentUpdateQueue上增加一個effect,並且更新當前hook的memoizedState值
sideEffectTag |= fiberEffectTag;
hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
這一段有個重點: useEffect的依賴項沒變化的時候,componentUpdateQueue增加一個tag爲NoHookEffect
= 0 的effect
- 那麼他是怎麼比較deps的依賴項有沒有更新的呢?
unction areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) {
// ... 省掉一些_Dev_處理
return false;
}
// ... 再次省掉一些_Dev_處理
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
哦,其實就是遍歷deps數組,對每一項執行Object.is()方法
- 最後,我們來看一下useEffect什麼時候執行,這得從react的函數組件的生命週期相關的調度開始。
在fiber的調度過程中,最終追溯到commitBeforeMutationLifeCycles
方法,在這裏會根據組件類型,去執行對應的生命週期,FunctionComponent
組件執行commitHookEffectList
方法
function commitHookEffectList(
unmountTag: number,
mountTag: number,
finishedWork: Fiber,
) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
// 上面說到 NoHookEffect = 0
// 當effect.tag是0的時候,跟誰做與運算都會得到0, 即不執行任何操作
do {
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount
const create = effect.create;
effect.destroy = create();
// _Dev_中的一些警告處理省略掉
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
由源碼可知,此時effect鏈表裏的hook會被依次執行;
在執行的時候,會把effect.tag跟一個變量做與運算;
然後判斷跟NoHookEffect(值爲0)是否相等。
所以當effect.tag的值是0,不管跟誰做與運算,結果都是0 ,這個時候不會執行更新。(前面也說到,當useEffect的依賴項沒更新時候,會聲明一個tag是0的effect,所以這個時候,組件不會發生更新。)
到此,useEffect的源碼就粗略的看過一遍了。
大概過程是函數組件在掛載階段會執行MountEffect,維護hook的鏈表,同時專門維護一個effect的鏈表。
在組件更新階段,會執行UpdateEffect,判斷deps有沒有更新,如果依賴項更新了,就執行useEffect裏操作,沒有就給這個effect標記一下NoHookEffect,跳過執行,去下一個useEffect