React 新特性useEffect

React16中新增的hooks特性进一步强化了函数组件的功能。本篇承接上次欧超对useState的源码的解读,分析一下另一个高频使用的hook useEffect的在react中的执行机制。

1.useEffect 解决了哪些问题?

1.函数组件没有生命周期。

2.ajax、事件绑定等业务逻辑耦合在生命周期中

3.业务逻辑散乱在不同的生命周期中

Effect Hook 可以让你在函数组件中执行副作用操作。数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。类比于class component,可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

2.useEffect 用法回顾

2.1 基本用法举例

function App() {
const [count, setCount] = useState(0);
const handleScroll = () => {
    setCount(count+1);
};
useEffect(()=>{
// create
    window.addEventListener('scroll',handleScroll);
return ()=>{
// destroy
    window.removeEventListener('scroll',handleScroll);
}
// deps
},[count]);
return <div className="wrapper">
    <div className="inner">
    {`It's the ${count} times to trigger scroll.`}
    </div>
</div>
}

3.useEffect 源码解读+图解

3.1 代码执行流程图
react版本:16.13.1
下面从react中fiber的两个阶段 render 阶段和commit阶段来分析useEffect。


代码执行流程图

3.2 部分全局变量简介

1)currentlyRenderingFiber$1
本质是workInProgressFiber,当前正在更新的Fiber.这里是作者为区别于workInProgressHook

// The work-in-progress fiber. I've named it differently to distinguish it from
// the work-in-progress hook.
let currentlyRenderingFiber: Fiber = (null: any);

2)workInProgressHook,全局变量,保存work-in-process fiber上最新的一个hook
这个是对调度schedule做的一个优化,在render阶段被更新打断时,变量workInProgress hook 暂存被打断的hook。

// work-in-progress hook list is a new list that will be added to the
  // work-in-progress fiber.
var workInProgressHook = null; 

3)finishedWork 指向当前已经完成准备工作的Fiber Tree Root。render阶段结束后会生成finishedWork.

3.3 Render 阶段

3.3.1 第一次调用useEffect

1)代码位置:react > ReactHooks.js

function useEffect(create, deps) {
    var dispatcher = resolveDispatcher();
    return dispatcher.useEffect(create, deps);
  }

① resolveDispatcher

function resolveDispatcher() {
    var dispatcher = ReactCurrentDispatcher.current;

    if (!(dispatcher !== null)) {
      {
        throw Error( "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem." );
      }
    }

    return dispatcher;
  }

② ReactCurrentDispatcher.current

renderWithHooks中根据current !== null && current.memoizedState !== null判断是首次渲染还是更新,对ReactCurrentDispatcher.current赋值。

ReactCurrentDispatcher.current的取值逻辑

HooksDispatcherOnMountInDEV和HooksDispatcherOnUpdateInDEV都是包含多个方法的对象。首次渲染时ReactCurrentDispatcher.current被赋值为HooksDispatcherOnMountInDEV,所以dispatcher.useEffect就是HooksDispatcherOnMountInDEV中的useEffect方法.

3.3.2 HooksDispatcherOnMountInDEV.useEffect

代码位置:react-dom > react-reconciler > ReactFiberHooks.js

  HooksDispatcherOnMountInDEV = {
      // ... other functions
      useEffect: function (create, deps) {
        currentHookNameInDev = 'useEffect';
        mountHookTypesDev();
        checkDepsAreArrayDev(deps);
        return mountEffect(create, deps);
      },
      // ... other functions
  }
    };

HooksDispatcherOnMountInDEV中的useEffect方法实际调用的是 mountEffect 方法,入参create是我们在useEffect的第一个参数,这里是一个函数,入参deps是useEffect第二个参数,是useEffect依赖的数组。

3.3.3 mountEffect

function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  if (__DEV__ = false) {
    // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
    if ('undefined' !== typeof jest) {
      warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
    }
  }
  return mountEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,
    create,
    deps,
  );
}

mountEffect实际调用的是 mountEffectImpl,mountEffectImpl除了透传create和deps还传入两个effectTag。
先看第一个effectTag.
UpdateEffect 、 PassiveEffect 对应 ReactSideEffectTags中的Update、Passive。

3.3.4 ReactSideEffectTags

代码位置:react-dom > react-reconciler > ReactSideEffectTags.js

export type SideEffectTag = number;

// Don't change these two values. They're used by React Dev Tools.
export const NoEffect = /*                 */ 0b00000000000000;
export const PerformedWork = /*            */ 0b00000000000001;

// You can change the rest (and add more).
export const Placement = /*                */ 0b00000000000010;
export const Update = /*                   */ 0b00000000000100;
export const PlacementAndUpdate = /*       */ 0b00000000000110;
export const Deletion = /*                 */ 0b00000000001000;
export const ContentReset = /*             */ 0b00000000010000;
export const Callback = /*                 */ 0b00000000100000;
export const DidCapture = /*               */ 0b00000001000000;
export const Ref = /*                      */ 0b00000010000000;
export const Snapshot = /*                 */ 0b00000100000000;
export const Passive = /*                  */ 0b00001000000000;
export const PassiveUnmountPendingDev = /* */ 0b10000000000000;
export const Hydrating = /*                */ 0b00010000000000;
export const HydratingAndUpdate = /*       */ 0b00010000000100;

// Passive & Update & Callback & Ref & Snapshot
export const LifecycleEffectMask = /*      */ 0b00001110100100;

// Union of all host effects
export const HostEffectMask = /*           */ 0b00011111111111;

export const Incomplete = /*               */ 0b00100000000000;
export const ShouldCapture = /*            */ 0b01000000000000;

ReactSideEffectTags 是fiber的副作用标志,用来表示fiber的副作用。
包括fiber的插入、更新、删除等等。

HookPassive对应ReactHookEffectTags中的Passive.

3.3.5 ReactHookEffectTags

代码位置:react-dom > react-reconciler > ReactHookEffectTags.js

export type HookEffectTag = number;

export const NoEffect = /*  */ 0b000;

// Represents whether effect should fire.
export const HasEffect = /* */ 0b001;

// Represents the phase in which the effect (not the clean-up) fires.
// useLayoutEffect的标志
export const Layout = /*    */ 0b010;
// useEffect的标志
export const Passive = /*   */ 0b100;

ReactHookEffectTags用来表示hook effect的副作用标志。

effectTag采用了复合类型方案设计。通过将二进制0的不同位上的数字置为1来表示不同的tag。
1)|运算可以将不同tag组合成为复合类型。
2)&运算可以用于判断复合类型中是否含有某个Tag.
3)通过A&=~B的方式,可以从复合类型A中去掉某个tagB。

3.3.6 mountEffectImpl

代码位置:react-dom > react-reconciler > ReactFiberHooks.js

 function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
  // 创建一个新的hook,添加到hook链表末尾并返回作为workInProcessHook
    var hook = mountWorkInProgressHook();
    var nextDeps = deps === undefined ? null : deps;
    // 将currentlyRenderingFiber$1.effectTag上fiberEffectTag的标志位置为1
    currentlyRenderingFiber$1.effectTag |= fiberEffectTag;
    // 创建新的effect,加到currentlyRenderingFiber$1.updateQueue.lastEffect上
    // 将pushEffect返回的新effect挂在workInProgressHook的memoizedState上
    hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, undefined, nextDeps);
  }

1)mountWorkInProgressHook
作用:创建一个新的hook添加到hook链表末尾, 赋值给全局变量workInProcessHook,并返回workInProcessHook。

function mountWorkInProgressHook() {
    var hook = {
      memoizedState: null,
      baseState: null,
      baseQueue: null,
      queue: null,
      next: null
    };
    // 如果全局变量workInProcessHook为空
    if (workInProgressHook === null) {
      // This is the first hook in the list
      // 如果是当前fiber的第一个hook, 将hook挂到work-in-progress fiber的memoizedState上
      currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
    } else {
      // Append to the end of the list
      // 如果全局变量workInProcessHook不为空
      workInProgressHook = workInProgressHook.next = hook;
    }

    return workInProgressHook;
  }
mountWorkInProgressHook方法

currentlyRenderingFiber 是一个全局变量,保存正在render的Fiber(也就是work-in-process fiber),hooks会以链表的形式保存在work-in-process fiber 的memoizedState字段上。

workInProgressHook,全局变量,保存work-in-process fiber上最新的一个hook。
2)将work-in-process fiber的effectTag 的 fiberEffectTag标志位置1。

currentlyRenderingFiber$1.effectTag |= fiberEffectTag;

接下来调用pushEffect这个方法.

hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, undefined, nextDeps);

HasEffect代表当前的useEffect hook需要更新。

位置 react-reconciler > src > ReactHookEffectTags.js

 var HasEffect =
  /* */
  1; // Represents the phase in which the effect (not the clean-up) fires.

hookEffectTag对应ReactHookEffectTags中的Passive,代表useEffect
create和deps是透传的useEffect的两个参数。
destroy 传入的是undefined.
3)pushEffect

 function pushEffect(tag, create, destroy, deps) {
    var effect = {
      tag: tag,
      create: create,
      destroy: destroy,
      deps: deps,
      // Circular
      next: null
    };
    var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;

    if (componentUpdateQueue === null) {
      // 创建函数组件更新队列挂到work-in-process fiber的updateQueue字段上。
      componentUpdateQueue = createFunctionComponentUpdateQueue();
      currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
      // 将第一个effect加入循环链表
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      var lastEffect = componentUpdateQueue.lastEffect;
      if (lastEffect === null) {
        componentUpdateQueue.lastEffect = effect.next = effect;
      } else {
        // 将最新的effect加入循环链表
        var firstEffect = lastEffect.next;
        lastEffect.next = effect;
        effect.next = firstEffect;
        componentUpdateQueue.lastEffect = effect;
      }
    }

    return effect;
  }

①如果workInProgressFiber的更新队列为空,会调用createFunctionComponentUpdateQueue创建一个空的更新队列

function createFunctionComponentUpdateQueue() {
    return {
      lastEffect: null
    };
  }

pushEffect这个方法的作用:
a. 将新创建的effect挂到workInProgressHook.updateQueue.lastEffect上。
b. 返回新创建的effect.


pushEffect方法
  1. pushEffect返回的新effect挂在workInProgressHook的memoizedState上
hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, undefined, nextDeps);
新effect挂在workInProgressHook的memoizedState上

首次调用时,useLayoutEffect与useEffect的代码执行流程完全相同,区别在于方法时用mountEffectImpl传入的fiberEffectTag和hookEffectTag不同。

function mountLayoutEffect(create, deps) {
    // 与mountEffect相同均调用mountEffectImpl,区别在于传入的tag不同。
    // fiberEffectTag 少传了Passive的标志;
    // hookEffectTag传入Layout,代表hookEffect中的useLayoutEffect
    return mountEffectImpl(Update, Layout, create, deps);
  }

3.3.7 第二次调用useEffect

当触发滚动事件时,首先触发setCount所绑定的dispatchAction, dispatchAction 调用 scheduleWork(fiber, expirationTime); 。。。层层调用直到在renderWithHooks调用var children = Component(props, secondArg);
Component 就是函数组件 function App. 执行完setCount,count 从0 变成1。接着调用useEffect。

 function useEffect(create, deps) {
    var dispatcher = resolveDispatcher();
    return dispatcher.useEffect(create, deps);
  }
ReactCurrentDispatcher.current的取值逻辑

更新时,dispatcher是HooksDispatcherOnUpdateInDEV,所以这里调用了HooksDispatcherOnUpdateInDEV.useEffect

    HooksDispatcherOnUpdateInDEV = {
        useEffect: function (create, deps) {
        currentHookNameInDev = 'useEffect';
        updateHookTypesDev();
        return updateEffect(create, deps);
      },
    };

HooksDispatcherOnUpdateInDEV.useEffect 调用的是updateEffect

3.3.8 updateEffect

function updateEffect(create, deps) {
    {
      // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
      if ('undefined' !== typeof jest) {
        warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber$1);
      }
    }

    return updateEffectImpl(Update | Passive, Passive$1, create, deps);
  }

updateEffect最终调用了updateEffectImpl这个方法,前两个参数是fiberEffectTag, hookEffectTag,后两个参数对应useEffect的两个入参。

3.3.9 updateEffectImpl

function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
    // 更新全局变量currentHook和workInProgressHook
    var hook = updateWorkInProgressHook();
    var nextDeps = deps === undefined ? null : deps;
    var destroy = undefined;

    if (currentHook !== null) {
      var prevEffect = currentHook.memoizedState;
      // destroy 是useEffect第一参数,传入的函数中返回的函数
      // destroy取的是上次更新的currentHook中的destroy
      destroy = prevEffect.destroy;
      // 依赖数组不存在,跳过依赖比较,必然触发更新,为了优化性能可以把依赖数组加上。
      if (nextDeps !== null) {
        var prevDeps = prevEffect.deps;
        // 比较本次和上次更新中useEffect的依赖变量是否变化
        if (areHookInputsEqual(nextDeps, prevDeps)) {
          //如果依赖没有发生改变,不会将hasEffect的标志位置1,调用pushEffect
          pushEffect(hookEffectTag, create, destroy, nextDeps);
          return;
        }
      }
    }
    // 将workInProgressHook的effectTag的fiberEffectTag标志位置1,在commit阶段会被用到。
    currentlyRenderingFiber$1.effectTag |= fiberEffectTag;
    // 如果依赖发生变化,将hasEffect的标志位置1,调用pushEffect,并赋值给workInProgressHook的memoizedState
    // hook的memoizedState是用来保存需要更新的effect
    hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, destroy, nextDeps);
  }

1)首先调用了updateWorkInProgressHook这个方法

  function updateWorkInProgressHook() {
    // This function is used both for updates and for re-renders triggered by a
    // render phase update. It assumes there is either a current hook we can
    // clone, or a work-in-progress hook from a previous render pass that we can
    // use as a base. When we reach the end of the base list, we must switch to
    // the dispatcher used for mounts.
    var nextCurrentHook;
    // 与mountWorkInProgressHook创建新hook不同,
    // 这里是从currentFiber的memoizedState上取hookList,通过next指针找到当前的hook
    if (currentHook === null) {// 如果是第一个hook,先找currentFiber,然后从currentFiber的memoizedState上取第一个hook
      var current = currentlyRenderingFiber$1.alternate;

      if (current !== null) {
        nextCurrentHook = current.memoizedState;
      } else {
        nextCurrentHook = null;
      }
    } else {// 非第一个hook的情况
      nextCurrentHook = currentHook.next;
    }

    var nextWorkInProgressHook;

    if (workInProgressHook === null) {
      nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
    } else {
      nextWorkInProgressHook = workInProgressHook.next;
    }
    // re-render 的情况,直接使用nextWorkInProgressHook
    if (nextWorkInProgressHook !== null) {
      // There's already a work-in-progress. Reuse it.
      workInProgressHook = nextWorkInProgressHook;
      nextWorkInProgressHook = workInProgressHook.next;
      currentHook = nextCurrentHook;
    } else {
      // Clone from the current hook.
      if (!(nextCurrentHook !== null)) {
        {
          throw Error( "Rendered more hooks than during the previous render." );
        }
      }
      // nextCurrentHook是上个更新或者渲染生成的对应当前workInProgressHook的hook
      // 更新currentHook这个全局变量
      currentHook = nextCurrentHook;
      //直接用currentFiber上的currentHook的属性生成一个新的hook
      var newHook = {
        memoizedState: currentHook.memoizedState,
        baseState: currentHook.baseState,
        baseQueue: currentHook.baseQueue,
        queue: currentHook.queue,
        next: null
      };

      if (workInProgressHook === null) {
        // This is the first hook in the list.
        currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
      } else {
        // Append to the end of the list.
        workInProgressHook = workInProgressHook.next = newHook;
      }
    }

    return workInProgressHook;
  }
// current hook list is the list that belongs to the current fiber. The
  // work-in-progress hook list is a new list that will be added to the
  // work-in-progress fiber.

  var currentHook = null;
  var workInProgressHook = null; 

这里分为两种情况,如果 nextWorkInProgressHook 存在那么就是 re-render,如果是 re-render 说明当前更新周期中还要继续处理 workInProgressHook。

如果不是 re-render,就生成一个新的 workInProgressHook。前面提到的mountWorkInProgressHook方法是用新的hook作为workInProgressHook,updateWorkInProgressHook这个方法用于基于首次渲染生成的hookList中的currentHook,生成一个workInProgressHook.
总之,updateWorkInProgressHook的作用有:
1.生成 workInProgressHook。
2.更新全局变量currentHook.

什么是re-render?
re-render指的是当前更新周期中产生了新的更新周期。标志是fiber===currentlyRenderingFiber

  1. 接着从全局变量currentHook的memoizedState属性上获取上次的effect.调用areHookInputsEqual(nextDeps, prevDeps)比较本次和上次更新中useEffect的依赖deps是否发生了变化。
    位置 react-reconciler > src > ReactFiberHooks.js
 function areHookInputsEqual(nextDeps, prevDeps) {
    {
      if (ignorePreviousDependencies) {
        // Only true when this component is being hot reloaded.
        return false;
      }
    }

    if (prevDeps === null) {
      {
        error('%s received a final argument during this render, but not during ' + 'the previous render. Even though the final argument is optional, ' + 'its type cannot change between renders.', currentHookNameInDev);
      }

      return false;
    }

    {
      // Don't bother comparing lengths in prod because these arrays should be
      // passed inline.
      if (nextDeps.length !== prevDeps.length) {
        error('The final argument passed to %s changed size between renders. The ' + 'order and size of this array must remain constant.\n\n' + 'Previous: %s\n' + 'Incoming: %s', currentHookNameInDev, "[" + prevDeps.join(', ') + "]", "[" + nextDeps.join(', ') + "]");
      }
    }
    //遍历依赖数组,用ObjectIs来比较依赖deps是否发生变化
    for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
      if (objectIs(nextDeps[i], prevDeps[i])) {
        continue;
      }

      return false;
    }

    return true;
  }

看下ObjectIs的实现:
位置react-dom.development\packages\shared\objectIs.js

  function is(x, y) {
    return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y // eslint-disable-line no-self-compare
    ;
  }

  var objectIs = typeof Object.is === 'function' ? Object.is : is;

如果比较后发现依赖deps不变,会调用pushEffect (hookEffectTag, create, destroy, nextDeps),此时没有将HasEffect标志位置1。表示没有 effect 需要执行,因此在 commit 阶段执行 unmount 和 mount时,因为effect的tag不同,不会调用 destroy 和 create 方法,然后 return.
如果 currentHook 等于 null 或是deps 发生改变,将传入的 fiberEffectTag 设置到当前 fiber 对象的 effectTag 上,最后调用 pushEffect,将hookEffectTag 的HasEffect标志位置1,(HasEffect | hookEffectTag),create,nextDeps,将 pushEffect 的返回的新effect赋值给 workInProgressHook的memoizedState。

更新调用时,useLayoutEffect与useEffect的代码执行流程完全相同,区别在于方法时用updateEffectImpl传入的fiberEffectTag和hookEffectTag不同。

function updateLayoutEffect(create, deps) {
    // 与updateEffect相同均调用updateEffectImpl,区别在于传入的tag不同。
    // fiberEffectTag 少传了Passive的标志;
    // hookEffectTag传入Layout,代表hookEffect中的useLayoutEffect
    return updateEffectImpl(Update, Layout, create, deps);
  }

到这里,useEffect就执行完了。


Render阶段流程图

可以看出初次渲染和更新时调用useEffect只是将hookList挂在workInProgressFiber上,以及将effectList挂到hook上和workInProgressFiber.updateQueue上。但是挂在workInProgressFiber.updateQueue上面的effect链表还没有被用到。

effectList会在哪里被用到呢?

React的有两个重要的阶段,render 阶段 和 commit阶段 。以上是在render阶段完成的,接下来会在commit阶段执行effectList。

3.4 commit 阶段

3.4.0 completeUnitOfWork

作用:构建effectFiber链

3.4.1 commitRoot

function commitRoot(root) {
    // 获取render时优先级
    var renderPriorityLevel = getCurrentPriorityLevel();
    // 以最高优先级ImmediatePriority执行第二个参数fn,fn会被立即执行
    runWithPriority$1(ImmediatePriority, commitRootImpl.bind(null, root, renderPriorityLevel));
    return null;
  }

commitRoot 是 commit阶段入口。
runWithPriority$1这个函数作用是先记录当前优先级,然后以指定的优先级执行函数,执行完毕会,恢复优先级。
这里会调用runWithPriority$1以最高优先级ImmediatePriority执行第二个参数commitRootImpl.bind(null, root, renderPriorityLevel)。

3.4.2 commitRootImpl

commitRootImpl 方法较为复杂,它主要负责执行 effectList上的更新,整个过程分为 before mutation,mutation 和 layout 三个阶段,每个阶段会执行不同的更新任务。
以before mutation 为例,

// ------------- before mutation 阶段开始  -----------------
      startCommitSnapshotEffectsTimer();
      prepareForCommit(root.containerInfo);
      nextEffect = firstEffect;

      do {
        {
          invokeGuardedCallback(null, commitBeforeMutationEffects, null);

          if (hasCaughtError()) {
            if (!(nextEffect !== null)) {
              {
                throw Error( "Should be working on an effect." );
              }
            }

            var error = clearCaughtError();
            captureCommitPhaseError(nextEffect, error);
            nextEffect = nextEffect.nextEffect;
          }
        }
      } while (nextEffect !== null);

      stopCommitSnapshotEffectsTimer();
      // ------------- before mutation 阶段结束  -----------------

这个阶段会遍历effectList,执行invokeGuardedCallback(null, commitBeforeMutationEffects, null);
在另外的mutation和layout2个阶段则会执行
invokeGuardedCallback(null, commitMutationEffects, null, root, renderPriorityLevel); 和
invokeGuardedCallback(null, commitLayoutEffects, null, root, expirationTime);
更新的核心是调用了commitBeforeMutationEffects,commitMutationEffects 和commitLayoutEffects三个方法。

3.4.3 commitBeforeMutationEffects

 function commitBeforeMutationEffects() {
    while (nextEffect !== null) {
      var effectTag = nextEffect.effectTag;

      if ((effectTag & Snapshot) !== NoEffect) {
        setCurrentFiber(nextEffect);
        recordEffect();
        var current = nextEffect.alternate;
        commitBeforeMutationLifeCycles(current, nextEffect);
        resetCurrentFiber();
      }
      // useEffect 的 effectTag曾传入Passive
      if ((effectTag & Passive) !== NoEffect) {
        // If there are passive effects, schedule a callback to flush at
        // the earliest opportunity.
        if (!rootDoesHavePassiveEffects) {
          rootDoesHavePassiveEffects = true;
          // NormalPriority 不是最高优先级,会推迟执行 flushPassiveEffects
          scheduleCallback(NormalPriority, function () {
            flushPassiveEffects();
            return null;
          });
        }
      }

      nextEffect = nextEffect.nextEffect;
    }
  }

flushPassiveEffects 是用于处理副作用的。最终会遍历effectList执行每个effect的create和destory函数。
NormalPriority 不是最高优先级,会推迟执行 flushPassiveEffects,所以最后再看flushPassiveEffects。

3.4.4 commitMutationEffects

function commitMutationEffects(root, renderPriorityLevel) {
    // TODO: Should probably move the bulk of this function to commitWork.
    while (nextEffect !== null) {
      setCurrentFiber(nextEffect);
      var effectTag = nextEffect.effectTag;

      if (effectTag & ContentReset) {
        commitResetTextContent(nextEffect);
      }

      if (effectTag & Ref) {
        var current = nextEffect.alternate;

        if (current !== null) {
          commitDetachRef(current);
        }
      } // The following switch statement is only concerned about placement,
      // updates, and deletions. To avoid needing to add a case for every possible
      // bitmap value, we remove the secondary effects from the effect tag and
      // switch on that value.

      var primaryEffectTag = effectTag & (Placement | Update | Deletion | Hydrating);

      switch (primaryEffectTag) {
        case Placement:
          {
            commitPlacement(nextEffect); // Clear the "placement" from effect tag so that we know that this is
            // inserted, before any life-cycles like componentDidMount gets called.
            // TODO: findDOMNode doesn't rely on this any more but isMounted does
            // and isMounted is deprecated anyway so we should be able to kill this.

            nextEffect.effectTag &= ~Placement;
            break;
          }

        case PlacementAndUpdate:
          {
            // Placement
            commitPlacement(nextEffect); // Clear the "placement" from effect tag so that we know that this is
            // inserted, before any life-cycles like componentDidMount gets called.

            nextEffect.effectTag &= ~Placement; // Update

            var _current = nextEffect.alternate;
            commitWork(_current, nextEffect);
            break;
          }

        case Hydrating:
          {
            nextEffect.effectTag &= ~Hydrating;
            break;
          }

        case HydratingAndUpdate:
          {
            nextEffect.effectTag &= ~Hydrating; // Update

            var _current2 = nextEffect.alternate;
            commitWork(_current2, nextEffect);
            break;
          }

        case Update:
          {
            var _current3 = nextEffect.alternate;
            commitWork(_current3, nextEffect);
            break;
          }

        case Deletion:
          {
            commitDeletion(root, nextEffect, renderPriorityLevel);
            break;
          }
      } // TODO: Only record a mutation effect if primaryEffectTag is non-zero.


      recordEffect();
      resetCurrentFiber();
      nextEffect = nextEffect.nextEffect;
    }
  }

commitMutationEffects 提交HostComponent的side effect,也就是DOM节点的操作(增删改), 这里和我们给出的例子中的useEffect 有关是update操作,这里会执行 commitWork函数。

3.4.5 commitWork

function commitWork(current, finishedWork) {

    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case MemoComponent:
      case SimpleMemoComponent:
      case Block:
        {
          // Layout effects are destroyed during the mutation phase so that all
          // destroy functions for all fibers are called before any create functions.
          // This prevents sibling component effects from interfering with each other,
          // e.g. a destroy function in one component should never override a ref set
          // by a create function in another component during the same commit.
          // 执行useLayoutEffect的destroy函数
          commitHookEffectListUnmount(Layout | HasEffect, finishedWork);
          return;
        }
        // 省略其他case。。。
      }
  }

commitWork作用是对DOM节点上的属性进行更新.这里对于FunctionComponent,它会执行useLayoutEffect的destroy函数,commitHookEffectListUnmount(Layout | HasEffect, finishedWork);

3.4.6 commitHookEffectListUnmount

function commitHookEffectListUnmount(tag, finishedWork) {
    var updateQueue = finishedWork.updateQueue;
    var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

    if (lastEffect !== null) {
      var firstEffect = lastEffect.next;
      var effect = firstEffect;

      do {
        if ((effect.tag & tag) === tag) {
          // Unmount
          var destroy = effect.destroy;
          effect.destroy = undefined;

          if (destroy !== undefined) {
            destroy();
          }
        }

        effect = effect.next;
      } while (effect !== firstEffect);
    }
  }

作用: 从finishedWork 这个fiber上取下updateQueue上的effectList,遍历effectList,执行tag对应的destroy函数。

3.4.7 commitLayoutEffects

function commitLayoutEffects(root, committedExpirationTime) {
    // TODO: Should probably move the bulk of this function to commitWork.
    while (nextEffect !== null) {
      setCurrentFiber(nextEffect);
      var effectTag = nextEffect.effectTag;
      // 当有Update/Callback的effectTag,执行commitLifeCycles(),执行effect.destroy()
      if (effectTag & (Update | Callback)) {
        recordEffect();
        var current = nextEffect.alternate;
        commitLifeCycles(root, current, nextEffect);
      }

      if (effectTag & Ref) {
        recordEffect();
        commitAttachRef(nextEffect);
      }

      resetCurrentFiber();
      nextEffect = nextEffect.nextEffect;
    }
  }

commitLifeCycles(root, current, nextEffect);

3.4.8 commitLifeCycles

function commitLifeCycles(finishedRoot, current, finishedWork, committedExpirationTime) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent:
      case Block:
        {
          // At this point layout effects have already been destroyed (during mutation phase).
          // This is done to prevent sibling component effects from interfering with each other,
          // e.g. a destroy function in one component should never override a ref set
          // by a create function in another component during the same commit.
          commitHookEffectListMount(Layout | HasEffect, finishedWork);

          return;
        }
      // 省略后面的代码。。。
    }
  }

因为当前是FunctionComponent,会执行commitHookEffectListMount(Layout | HasEffect, finishedWork); 遍历EffectList ,执行useLayoutEffect的create方法.

3.4.9 commitHookEffectListMount

function commitHookEffectListMount(tag, finishedWork) {
    var updateQueue = finishedWork.updateQueue;
    var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

    if (lastEffect !== null) {
      var firstEffect = lastEffect.next;
      var effect = firstEffect;

      do {
        if ((effect.tag & tag) === tag) {
          // Mount
          var create = effect.create;
          effect.destroy = create();

          {
            var destroy = effect.destroy;

            if (destroy !== undefined && typeof destroy !== 'function') {
              var addendum = void 0;

              if (destroy === null) {
                addendum = ' You returned null. If your effect does not require clean ' + 'up, return undefined (or nothing).';
              } else if (typeof destroy.then === 'function') {
                addendum = '\n\nIt looks like you wrote useEffect(async () => ...) or returned a Promise. ' + 'Instead, write the async function inside your effect ' + 'and call it immediately:\n\n' + 'useEffect(() => {\n' + '  async function fetchData() {\n' + '    // You can await here\n' + '    const response = await MyAPI.getData(someId);\n' + '    // ...\n' + '  }\n' + '  fetchData();\n' + "}, [someId]); // Or [] if effect doesn't need props or state\n\n" + 'Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching';
              } else {
                addendum = ' You returned: ' + destroy;
              }

              error('An effect function must not return anything besides a function, ' + 'which is used for clean-up.%s%s', addendum, getStackByFiberInDevAndProd(finishedWork));
            }
          }
        }

        effect = effect.next;
      } while (effect !== firstEffect);
    }
  }

这个函数作用是遍历finishedWork.updateQueue 上面的EffectList ,执行指定符合tag条件的 effect的create方法.并创建effect的destory 函数。

3.4.10 flushPassiveEffects

function flushPassiveEffects() {
    if (pendingPassiveEffectsRenderPriority !== NoPriority) {
      var priorityLevel = pendingPassiveEffectsRenderPriority > NormalPriority ? NormalPriority : pendingPassiveEffectsRenderPriority;
      pendingPassiveEffectsRenderPriority = NoPriority;
      return runWithPriority$1(priorityLevel, flushPassiveEffectsImpl);
    }
  }

flushPassiveEffects 会计算临时的优先级,用临时优先级执行回调 flushPassiveEffectsImpl

3.4.11 flushPassiveEffectsImpl

 function flushPassiveEffectsImpl() {
    if (rootWithPendingPassiveEffects === null) {
      return false;
    }

    var root = rootWithPendingPassiveEffects;
    var expirationTime = pendingPassiveEffectsExpirationTime;
    rootWithPendingPassiveEffects = null;
    pendingPassiveEffectsExpirationTime = NoWork;

    if (!((executionContext & (RenderContext | CommitContext)) === NoContext)) {
      {
        throw Error( "Cannot flush passive effects while already rendering." );
      }
    }

    var prevExecutionContext = executionContext;
    executionContext |= CommitContext;
    var prevInteractions = pushInteractions(root);

    {
      // Note: This currently assumes there are no passive effects on the root fiber
      // because the root is not part of its own effect list.
      // This could change in the future.
      var _effect2 = root.current.firstEffect;

      while (_effect2 !== null) {
        {
          setCurrentFiber(_effect2);
          invokeGuardedCallback(null, commitPassiveHookEffects, null, _effect2);

          if (hasCaughtError()) {
            if (!(_effect2 !== null)) {
              {
                throw Error( "Should be working on an effect." );
              }
            }

            var _error5 = clearCaughtError();

            captureCommitPhaseError(_effect2, _error5);
          }

          resetCurrentFiber();
        }

        var nextNextEffect = _effect2.nextEffect; // Remove nextEffect pointer to assist GC

        _effect2.nextEffect = null;
        _effect2 = nextNextEffect;
      }
    }

    {
      popInteractions(prevInteractions);
      finishPendingInteractions(root, expirationTime);
    }

    executionContext = prevExecutionContext;
    flushSyncCallbackQueue(); // If additional passive effects were scheduled, increment a counter. If this
    // exceeds the limit, we'll fire a warning.

    nestedPassiveUpdateCount = rootWithPendingPassiveEffects === null ? 0 : nestedPassiveUpdateCount + 1;
    return true;
  }

flushPassiveEffectsImpl 中会有一段类似于commitIRootImpl中的while循环
遍历effectList,执行 invokeGuardedCallback(null, commitPassiveHookEffects, null, _effect2);

3.4.6 commitPassiveHookEffects

function commitPassiveHookEffects(finishedWork) {
    if ((finishedWork.effectTag & Passive) !== NoEffect) {
      switch (finishedWork.tag) {
        case FunctionComponent:
        case ForwardRef:
        case SimpleMemoComponent:
        case Block:
          {
            // TODO (#17945) We should call all passive destroy functions (for all fibers)
            // before calling any create functions. The current approach only serializes
            // these for a single fiber.
            commitHookEffectListUnmount(Passive$1 | HasEffect, finishedWork);
            commitHookEffectListMount(Passive$1 | HasEffect, finishedWork);
            break;
          }
      }
    }
  }

这个函数的作用是:
1)遍历effectList,执行useEffect 的destory方法。
2)遍历effectList,执行useEffect 的create方法。


Commit阶段流程图

4.总结

4.1 render阶段:
在workInProgressFiber上创建(memoizedState)hookList 和(updateQueue)effectList
4.2 commit阶段:
遍历finishedWork 的(updateQueue)effectList,执行副作用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349