React Hook之useEffect的使用和源碼分析

作用

副作用

前端開發中的副作用一般有: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。

  1. 先來看一下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 | PassiveEffectUnmountPassive | MountPassive

  1. UpdateEffect | PassiveEffectUnmountPassive | MountPassive是什麼?
    答: 是一個二進制數字,用來標記是什麼類型的副作用的

  2. 繼續看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的返回值存了下來。

  1. 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的鏈表。

  1. 再來看看組件更新時調用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

  1. 那麼他是怎麼比較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()方法

  1. 最後,我們來看一下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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章