理解React Fiber

347 阅读3分钟

引用一段话,作为我对Fiber架构的理解

You can think of a fiber as a data structure that represents some work to do or, in other words, a unit of work. Fiber’s architecture also provides a convenient way to track, schedule, pause and abort the work.

Fiber架构是通过数据结构来代表任务的集合,可以基于这个架构来实现跟踪,调度,暂停,取消任务;

后面理解Fiber 架构的过程都是基于下面的示例代码

import React, { useEffect, useState, useRef, useCallback } from 'react';

export default function Test() {
  let name = 1;
  const logRef = useRef(null);
  const logRef2 = useRef(null);
  const logRef3 = useRef(null);
  const [a, setA] = useState('a');
  const [b, setB] = useState('b');
  const [c, setC] = useState('c');


  const handler = useCallback(() => {
    setA(a + a);
    setB(b + b);
    setC(c + c);
  }, [a, b, c]);

  return (
    <section>
      <header>{a}</header>
      <div>{b}</div>
      <b>{c}</b>
      <p onClick={handler}>Click Me</p>
    </section>
  );
}

React Fiber分成两个阶段 Render Phase Fiber建构中有两个树:currentworkInProgress

任务开始是从Root开始:

function performSyncWorkOnRoot(root) {
    ...
    do {
      try {
        workLoopSync();
        break;
      } catch (thrownValue) {
        handleError(root, thrownValue);
      }
    } while (true);
} 

在第一次render后,React会创建一个当前状态的树,这个树我们就是current树; 当React在遍历current树时,会为根据current上节点的alternate属性, 为每个Fiber Node创建workInProgress 节点,这样就构成了workInProgress tree;

workInProgress = createWorkInProgress(root.current, null);

function createWorkInProgress(current, pendingProps) {
  var workInProgress = current.alternate;

  if (workInProgress === null) {
  // ...
  } else {
    workInProgress.pendingProps = pendingProps; // We already have an alternate.
    // Reset the effect tag.

    workInProgress.effectTag = NoEffect; // The effect list is no longer valid.

    workInProgress.nextEffect = null;
    workInProgress.firstEffect = null;
    workInProgress.lastEffect = null;
  }
  // ...
  workInProgress.childExpirationTime = current.childExpirationTime;
  workInProgress.expirationTime = current.expirationTime;
  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue; // Clone the dependencies object. This is mutated during the render phase, so
  // it cannot be shared with the current fiber.
  return workInProgress;
} 

做好上面的准备后,开始深度遍历Node开始计算render过程了。

    do {
      try {
        workLoopSync();
        break;
      } catch (thrownValue) {
        handleError(root, thrownValue);
      }
    } while (true);
function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

这里的workInProgress就是上面createWorkInProgress中生成的,当前指向root.current.alternate;

后面开始从workInProgress出发,执行任务单元;

function performUnitOfWork(unitOfWork) {
  // The current, flushed, state of this fiber is the alternate. Ideally
  // nothing should rely on this, but relying on it here means that we don't
  // need an additional field on the work in progress.
  var current = unitOfWork.alternate;
  startWorkTimer(unitOfWork);
  setCurrentFiber(unitOfWork);
  var next;

  if ( (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork$1(current, unitOfWork, renderExpirationTime$1);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork$1(current, unitOfWork, renderExpirationTime$1);
  }

  resetCurrentFiber();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    next = completeUnitOfWork(unitOfWork);
  }

  ReactCurrentOwner$2.current = null;
  return next;
}

beginWork里面会根据当前Fiber Node的tag来判断如何执行; 比如当前tag=3代表是Root Node;

switch (workInProgress.tag) {
    case HostRoot: // 3
      pushHostRootContext(workInProgress);
      resetHydrationState();
      break;

    case HostComponent:
}

beginWork执行完成后,会返回下个要执行的任务单元:child 节点; 然后返回到performUnitOfWork阶段,继续执行下一个循环; 根据上面的代码,这里传入的unitOfWork为Root的child节点, 重复上面的操作:current指向unitOfWork.alternate; 在这个Demo里指向的是最外层的APP container,tag=5(HostComponet)

依次通过child属性进行遍历

Root -> App Div ->  Test FC

Current = child.alternate

同样到底,当我们遍历到section节点时,props中的children值会发生变化(父组件重新创建,会导致子组件都会重新render); 当render到header节点时,我们看下beginWork的执行情况

function beginWork(current, workInProgress, renderExpirationTime) {
  var updateExpirationTime = workInProgress.expirationTime;
  // ...

  if (current !== null) {
    var oldProps = current.memoizedProps;
    var newProps = workInProgress.pendingProps;

    if (oldProps !== newProps || hasContextChanged() || ( // Force a re-render if the implementation changed due to hot reload:
     workInProgress.type !== current.type )) {
      // If props or context changed, mark the fiber as having performed work.
      // This may be unset if the props are determined to be equal later (memo).
      didReceiveUpdate = true;
    } else if (updateExpirationTime < renderExpirationTime) {
      didReceiveUpdate = false;
   // ...

这里会涉及到Props的比较,比如这里setState之后:

后面开始进入updateComponet

function updateHostComponent(current, workInProgress, renderExpirationTime) {
  pushHostContext(workInProgress);

  // ...
  reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime);
  return workInProgress.child;
}

最终还是进入reconcileChildren方法

// reconcileChildren function

 workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderExpirationTime);

// 隔离

  function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, expirationTime) {
    // ...
    var resultingFirstChild = null;
    var previousNewFiber = null;
    var oldFiber = currentFirstChild;
    var lastPlacedIndex = 0;
    var newIdx = 0;
    var nextOldFiber = null;

    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }

      var newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], expirationTime);

      if (newFiber === null) {
        // TODO: This breaks on empty slots like null children. That's
        // unfortunate because it triggers the slow path all the time. We need
        // a better way to communicate whether this was a miss or null,
        // boolean, undefined, etc.
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }

        break;
      }

      if (shouldTrackSideEffects) {
        if (oldFiber && newFiber.alternate === null) {
          // We matched the slot, but we didn't reuse the existing fiber, so we
          // need to delete the existing child.
          deleteChild(returnFiber, oldFiber);
        }
      }

      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

      if (previousNewFiber === null) {
        // TODO: Move out of the loop. This only happens for the first run.
        resultingFirstChild = newFiber;
      } else {
        // TODO: Defer siblings if we're not at the right index for this slot.
        // I.e. if we had null values before, then we want to defer this
        // for each null value. However, we also don't want to call updateSlot
        // with the previous one.
        previousNewFiber.sibling = newFiber;
      }

      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    if (newIdx === newChildren.length) {
      // We've reached the end of the new children. We can delete the rest.
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }

比如这里会创建一个纯文本, updateSlot最终会调用下面的方式,借用createWorkInProgress方案实现新建一个Fiber Node

  function useFiber(fiber, pendingProps) {
    // We currently set sibling to null and index to 0 here because it is easy
    // to forget to do before returning it. E.g. for the single child case.
    var clone = createWorkInProgress(fiber, pendingProps);
    clone.index = 0;
    clone.sibling = null;
    return clone;
  }

最终会通过useFiber来实现新建一个fiber Node,作为nextNewFiber; 最终返回的resultingFirstChild就是header的第一个子节点,文本节点status; 并且 workInProgress.child指向当前节点,并且把这个节点作为performUnitOfWork的参数开始执行,最终会返回一个null;

现在把执行完header,p,span,b等Component之后,我们继续执行performSyncWorkOnRoot

  root.finishedWork = root.current.alternate;
  root.finishedExpirationTime = expirationTime;
  finishSyncRender(root);

finishedWork就代表了当前经过第一调制阶段后的结果:在这之前我们叫workInProgress; 更新之后,这个树会变成下个render阶段的current;也就是说当前finishedWork上面的状态已经是最新的状态,真正需要render的diff是这里的effect list;

最终我们从root节点开始,

root.child
--firstEffect--> header 
--nextEffect--> div 
--nextEffect--> b
--nextEffect--> p(useCallback会导致函数重新生成)

root.child
--lastEffect--> p

这里Effect的顺序因为是深度遍历的,如果你把上面代码改成 <header>{a}{b}</header

你会发现,firstEffect是指向的 FiberNode a , 然后nextEffect指向的是Fiber Node b 两个的tag类型都是hostText;

结果生成后,开始进入commit阶段

Commit
function finishSyncRender(root) {
  // Set this to null to indicate there's no in-progress render.
  workInProgressRoot = null;
  commitRoot(root);
}

function runWithPriority$1(reactPriorityLevel, fn) {
  var priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
  return Scheduler_runWithPriority(priorityLevel, fn);
}

根据优先级来执行任务

firstEffect = finishedWork.firstEffect;

拿到第一个effect入口

function invokeGuardedCallback(name, func, context, a, b, c, d, e, f) {
  hasError = false;
  caughtError = null;
  invokeGuardedCallbackImpl$1.apply(reporter, arguments);
}

事件绑定:

var evtType = "react-" + (name ? name : 'invokeguardedcallback'); 
// evtType: react-invokeguardedcallback
fakeNode.addEventListener(evtType, callCallback, false);
evt.initEvent(evtType, false, false);
fakeNode.dispatchEvent(evt);

这里callCallback会执行下面函数

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

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

    switch (primaryEffectTag) {
     // ...

      case Update:
        {
          var _current3 = nextEffect.alternate;
         // current3: a; nextEffect: aa
          commitWork(_current3, nextEffect);
          break;
        }
    // ...
    
    recordEffect();
    resetCurrentFiber();
    nextEffect = nextEffect.nextEffect;
  }

然后coomitWork

    case HostText:
      {
        if (!(finishedWork.stateNode !== null)) {
          {
            throw Error( "This should have a text node initialized. This error is likely caused by a bug in React. Please file an issue." );
          }
        }
        // stateNode:Text Dom Node
        var textInstance = finishedWork.stateNode;
        var newText = finishedWork.memoizedProps; // For hydration we reuse the update path but we treat the oldProps
        // as the newProps. The updatePayload will contain the real change in
        // this case.

        var oldText = current !== null ? current.memoizedProps : newText;
        commitTextUpdate(textInstance, oldText, newText);
        return;
      }

最终执行了

function commitTextUpdate(textInstance, oldText, newText) {
  textInstance.nodeValue = newText;
}

这里汇总了一些基本的DOM 的操作API 然后再获取下一个nextEffect nextEffect = nextEffect.nextEffect;

另外单独看下FC的render过程:

    case FunctionComponent:
      {
        var _Component = workInProgress.type;
        var unresolvedProps = workInProgress.pendingProps;
        var resolvedProps = workInProgress.elementType === _Component ? unresolvedProps : resolveDefaultProps(_Component, unresolvedProps);
        return updateFunctionComponent(current, workInProgress, _Component, resolvedProps, renderExpirationTime);
      }

function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderExpirationTime) {
 
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.expirationTime = NoWork; // The following should have already 

  //...

  var children = Component(props, secondArg); // Check if there was a render phase update

  if (workInProgress.expirationTime === renderExpirationTime) {
    // Keep rendering in a loop for as long as render phase updates continue to
    // be scheduled. Use a counter to prevent infinite loops.
    var numberOfReRenders = 0;

    do {
      workInProgress.expirationTime = NoWork;

      if (!(numberOfReRenders < RE_RENDER_LIMIT)) {
        {
          throw Error( "Too many re-renders. React limits the number of renders to prevent an infinite loop." );
        }
      }

      numberOfReRenders += 1;

      {
        // Even when hot reloading, allow dependencies to stabilize
        // after first render to prevent infinite render phase updates.
        ignorePreviousDependencies = false;
      } // Start over from the beginning of the list


      currentHook = null;
      workInProgressHook = null;
      workInProgress.updateQueue = null;

      {
        // Also validate hook order for cascading updates.
        hookTypesUpdateIndexDev = -1;
      }

      ReactCurrentDispatcher.current =  HooksDispatcherOnRerenderInDEV ;
      children = Component(props, secondArg);
    } while (workInProgress.expirationTime === renderExpirationTime);
  } // We can assume the previous dispatcher is always this one, since we set it
  // at the beginning of the render phase and there's no re-entrancy.


  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  {
    workInProgress._debugHookTypes = hookTypesDev;
  } // This check uses currentHook so that it works the same in DEV and prod bundles.
  // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.


  var didRenderTooFewHooks = currentHook !== null && currentHook.next !== null;
  renderExpirationTime = NoWork;
  currentlyRenderingFiber$1 = null;
  currentHook = null;
  workInProgressHook = null;

  {
    currentHookNameInDev = null;
    hookTypesDev = null;
    hookTypesUpdateIndexDev = -1;
  }

  didScheduleRenderPhaseUpdate = false;

  if (!!didRenderTooFewHooks) {
    {
      throw Error( "Rendered fewer hooks than expected. This may be caused by an accidental early return statement." );
    }
  }

  return children;
}

Componet(props, secondArg)会返回一个Object对象,里面有描述FC render之后的描述信息;

并且会返回nextChild,比如这里就是section; 最终会进入render child的阶段

function reconcileChildFibers(returnFiber, currentFirstChild, newChild, expirationTime)
{ 
 if (isObject) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, expirationTime));

        case REACT_PORTAL_TYPE:
          return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, expirationTime));
      }
    }

最终还是会返回一个Fiber Node,并把workInProgress.child指向这个Node

 workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderExpirationTime);

最上面updateFunctionComponent对FC的执行,也就返回这个child Node; 并把开始下一轮的循环执行:performUnitOfWork

  while (workInProgress !== null) {
    workInProgress = performUnitOfWork(workInProgress);
  }

依次深度遍历执行,直到遍历完所有Fiber Node,生成workInProgress Tree,并且计算完effect-list;

推荐阅读

indepth.dev/inside-fibe…