美文网首页react
react浅析1 初始渲染

react浅析1 初始渲染

作者: 百里哈哈 | 来源:发表于2020-09-08 09:25 被阅读0次

该分析基于React v16.13.1
首先我们从一个简单的示例, 看看此时的render的过程都是经历的哪里

// 该示例只是简单的渲染以下几个节点
<div>
      <span> span </span>
      <p>
          <span>tow</span>
          <em>three</em>
      </p>
  </div>

追踪源码其主要流程如下图所示


render.png

有图示可知,其过程主要有两个阶段

  1. 第一阶段workloop循环执行所需进行的work工作流, 在work过程中进行fiber的调和过程;调和包括fiber的构建,新建、变更、删除的effect的生成。
  2. 第二阶段commit阶段, 在commit阶段主要执行effects

涉及到的函数部分简单说明, 部分贴出源码主要逻辑进行说明
. legacyRenderSubtreeIntoContainer
. legacyRenderSubtreeIntoContainer,render函数调用时会生成一个root对象(对于hydrate会进行区分), 然后进入unbatchedUpdates调用。
其中生成root的方法调用过程如下:
legacyCreateRootFromDOMContainer -> createLegacyRoot -> ReactDOMBlockingRoot ->createRootImpl -> createContainer -> createFiberRoot -> FiberRootNode
. unbatchedUpdates非批次更新方法, 将直接进行updateContainer方法的调用
. updateContainer调用 enqueueUpdate 生成update连, 然后进入scheduleWork的调用
. scheduleWork到beginWord之间为任务调动过程,将放入任务调动章节进行说明

单向环状链

react中有多出用到单向,以enqueueUpdate方法为例,对其构建过程做一下简述。
该方法将待更新的update对象放入updateQueue中, updateQueue为一个环状单向链表
该链表的实现过程如下图, 其中pd表示pending、up表示一系列的update对象;该图演示链表为空第一次插入update以及第二次第三次插入。


QQ20200908-0.JPG
function enqueueUpdate(fiber, update) {
  var updateQueue = fiber.updateQueue;

  if (updateQueue === null) {
    // Only occurs if the fiber has been unmounted.
    return;
  }

  var sharedQueue = updateQueue.shared;
  var pending = sharedQueue.pending;

  if (pending === null) {
    // This is the first update. Create a circular list.
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }

  sharedQueue.pending = update;
  ...
}

fiber结构图

在介绍调和阶段之前, 可以先看一下最终生成的fiber树以便理解其遍历的过程
结构图如下,注意 app上还有rootFiber由于疏忽忘记画。

QQ20200908-1.JPG
a. 蓝线表示child
b. 绿线表示return指向父节点
c. 紫线表示sibling。

fiber树采用的是长子兄弟表示法, 在深度遍历的过程中先是进行child的递归遍历,
如果child不存在则访问sibling
如果sibling也不存在则返回到父节点,查看其对应的sibling是否存在继续进行递归

performUnitOfWork

该函数主要通过两个方法进行next(下一次调用任务)的返回,

  1. 通过beginWork最终进行调和后获取workInProgress.child作为下一次的任务, 如果当前节点已没有子孩子说明已遍历到叶子节点此后将进入第二阶段
  2. completeUnitOfWork表明当前节点已完成调和过程, 需对其进行completeWork方法的调用,接下来继续遍历未完成的点。
function performUnitOfWork(unitOfWork) {
  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;
}

updateHostRoot

其主要代码大体如下

  1. workInProgress对象上的update队列克隆到当前的current对象上
  2. 执行update队列中的方法
  3. 进入调和阶段
function updateHostRoot(current, workInProgress, renderExpirationTime) {
  pushHostRootContext(workInProgress);
  var updateQueue = workInProgress.updateQueue;
  ...
  var nextProps = workInProgress.pendingProps;
  var prevState = workInProgress.memoizedState;
  var prevChildren = prevState !== null ? prevState.element : null;
  cloneUpdateQueue(current, workInProgress);
  processUpdateQueue(workInProgress, nextProps, null, renderExpirationTime);
  var nextState = workInProgress.memoizedState; // Caution: React DevTools currently depends on this property
  // being called "element".
  // jsx转化后对应的元素结构
  var nextChildren = nextState.element;
  ...

  var root = workInProgress.stateNode;

  if (root.hydrate && enterHydrationState(workInProgress)) {
   ...
  } else {
    // Otherwise reset hydration state in case we aborted and resumed another
    // root.
    reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime);
    resetHydrationState();
  }

  return workInProgress.child;
}

reconcileChildren

进行子节点的调和, 如果是首次渲染的话则执行mountChildFibers方法, mountChildFibers和reconcileChildFibers则都是对 ChildReconciler 方法进行调用

function reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime) {
  if (current === null) {
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderExpirationTime);
  } else {
    workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderExpirationTime);
  }
}

ChildReconciler

ChildReconciler对于不同类型的节点进行调和, 或生成、删除、复用fiber节点。
有下代码可知,它调和的类型包括element、portal、 string、Array等。
以单个的textNode为例
如果父元素的第一个节点的值为textNode类型,则复用其首个子元素fiber, 删除其他元素
如果不是则删除父节点下的所有元素,生成新的fiber

function ChildReconciler(shouldTrackSideEffects) {
  function placeSingleChild(newFiber) {
    // This is simpler for the single child case. We only need to do a
    // placement for inserting new children.
    if (shouldTrackSideEffects && newFiber.alternate === null) {
      newFiber.effectTag = Placement;
    }

    return newFiber;
  }
  ...
  function reconcileSingleTextNode(returnFiber, currentFirstChild, textContent, expirationTime) {
    // There's no need to check for keys on text nodes since we don't have a
    // way to define them.
    if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
      // We already have an existing node so let's just update it and delete
      // the rest.
      deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
      var existing = useFiber(currentFirstChild, textContent);
      existing.return = returnFiber;
      return existing;
    } // The existing first child is not a text node so we need to create one
    // and delete the existing ones.


    deleteRemainingChildren(returnFiber, currentFirstChild);
    var created = createFiberFromText(textContent, returnFiber.mode, expirationTime);
    created.return = returnFiber;
    return created;
  }
  ...
  function reconcileChildFibers(returnFiber, currentFirstChild, newChild, expirationTime) {
    var isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null;

    if (isUnkeyedTopLevelFragment) {
      newChild = newChild.props.children;
    } // Handle object types


    var isObject = typeof newChild === 'object' && newChild !== null;

    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));
      }
    }

    if (typeof newChild === 'string' || typeof newChild === 'number') {
      return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, expirationTime));
    }

    if (isArray$1(newChild)) {
      return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, expirationTime);
    }

    if (getIteratorFn(newChild)) {
      return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, expirationTime);
    }

    if (isObject) {
      throwOnInvalidObjectType(returnFiber, newChild);
    }
    ...
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }

  return reconcileChildFibers;
}

completeUnitOfWork

当前fiber已完成调和时, 会调用completeUnitOfWork方法。

  1. 在该方法中首先会执行当前fiber的completeWork完成回调函数
  2. 获取next的值, 如果不存在; 则获取fiber.sibling, 如果sibling值存在则退出循环进入下一次的workloop
  3. 如果sibling不存在, 则回退到父节点,继续迭代至步骤1
    完成后会对副作用进行收集, 收集的副作用会形成一条链挂载到根节点上
function completeUnitOfWork(unitOfWork) {
  workInProgress = unitOfWork;

  do {
    var current = workInProgress.alternate;
    var returnFiber = workInProgress.return; // Check if the work completed or if something threw.

    if ((workInProgress.effectTag & Incomplete) === NoEffect) {
      setCurrentFiber(workInProgress);
      var next = void 0;

      if ( (workInProgress.mode & ProfileMode) === NoMode) {
        next = completeWork(current, workInProgress, renderExpirationTime$1);
      } else {
        startProfilerTimer(workInProgress);
        next = completeWork(current, workInProgress, renderExpirationTime$1); // Update render duration assuming we didn't error.

        stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
      }

      stopWorkTimer(workInProgress);
      resetCurrentFiber();
      resetChildExpirationTime(workInProgress);

      if (next !== null) {
        // Completing this fiber spawned new work. Work on that next.
        return next;
      }

      if (returnFiber !== null && // Do not append effects to parents if a sibling failed to complete
      (returnFiber.effectTag & Incomplete) === NoEffect) {
        // Append all the effects of the subtree and this fiber onto the effect
        // list of the parent. The completion order of the children affects the
        // side-effect order.
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = workInProgress.firstEffect;
        }

        if (workInProgress.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
          }

          returnFiber.lastEffect = workInProgress.lastEffect;
        } // If this fiber had side-effects, we append it AFTER the children's
        // side-effects. We can perform certain side-effects earlier if needed,
        // by doing multiple passes over the effect list. We don't want to
        // schedule our own side-effect on our own list because if end up
        // reusing children we'll schedule this effect onto itself since we're
        // at the end.


        var effectTag = workInProgress.effectTag; // Skip both NoWork and PerformedWork tags when creating the effect
        // list. PerformedWork effect is read by React DevTools but shouldn't be
        // committed.

        if (effectTag > PerformedWork) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress;
          } else {
            returnFiber.firstEffect = workInProgress;
          }

          returnFiber.lastEffect = workInProgress;
        }
      }
    } else {
      ...
    }

    var siblingFiber = workInProgress.sibling;

    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      return siblingFiber;
    } // Otherwise, return to the parent
    workInProgress = returnFiber;
  } while (workInProgress !== null); // We've reached the root.


  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }

  return null;
}

在workloop的过程中, 先是深度访问其child的节点, 当当前节点已无child(即为叶子节点)时说明当前节点已完成。
此后需回退至父节点继续访问, 整个完成过程为树的后序遍历
还是以最初的示例为例, 其遍历即完成的图示如下


QQ20200908-2.JPG

a. 蓝线表示child
b. 绿线表示return指向父节点
c. 紫线表示sibling。
d. 红色表示访问顺序
e. 橙色表示完成顺序。

current 和 workInProgress

current树表示的当前已经渲染并呈现在视图中的树, workInProgress是结构发生变更接下来将要渲染在视图中的树。
通过alternate属性可建立current 和 workInProgress的相互引用,react采用两棵树交替进行的方式避免update更新时重新生成一颗新树,从而减小性能的开销。

commit阶段

commit阶段为提交阶段,此阶段进行effect连的执行, 从初始来说该阶段会生成一个副作用, 通过该effect实现DOM元素挂载到根本从而实现view的渲染。

commitRoot

function commitRoot(root) {
  var renderPriorityLevel = getCurrentPriorityLevel();
  runWithPriority$1(ImmediatePriority, commitRootImpl.bind(null, root, renderPriorityLevel));
  return null;
}

commitRootImpl

该方法最主要的功能是对commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects这三种方法的调用。
我们知道作为MV架构必不可少的生命周期会存在一些回调的hooks钩子函数, 在此处会有体现。 该函数会分别循环遍历effect连,依次执行上面的三种方法。

  1. commitBeforeMutationEffects 在元素挂载到页面之前进行设置的钩子函数会在此处进行
  2. commitMutationEffects 实现DOM元素的变更, 最终体现视图渲染。
    以初次渲染为例, 其调用轨迹commitMutationEffects -> commitPlacement -> insertOrAppendPlacementNodeIntoContainer -> appendChildToContainer
  3. commitLayoutEffects 渲染完成之后的钩子函数会在此处进行
function commitRootImpl(root, renderPriorityLevel) {
  do {
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);

  var finishedWork = root.finishedWork;
  var expirationTime = root.finishedExpirationTime;

  if (finishedWork === null) {
    return null;
  }

  ...
  startCommitTimer(); // Update the first and last pending times on this root. The new first
  // pending time is whatever is left on the root fiber.

  var remainingExpirationTimeBeforeCommit = getRemainingExpirationTime(finishedWork);
  markRootFinishedAtTime(root, expirationTime, remainingExpirationTimeBeforeCommit);

  if (root === workInProgressRoot) {
    // We can reset these now that they are finished.
    workInProgressRoot = null;
    workInProgress = null;
    renderExpirationTime$1 = NoWork;
  } // This indicates that the last root we worked on is not the same one that
  // we're committing now. This most commonly happens when a suspended root
  // times out.
  // Get the list of effects.


  var firstEffect;

  if (finishedWork.effectTag > PerformedWork) {
    // A fiber's effect list consists only of its children, not itself. So if
    // the root has an effect, we need to add it to the end of the list. The
    // resulting list is the set that would belong to the root's parent, if it
    // had one; that is, all the effects in the tree including the root.
    if (finishedWork.lastEffect !== null) {
      finishedWork.lastEffect.nextEffect = finishedWork;
      firstEffect = finishedWork.firstEffect;
    } else {
      firstEffect = finishedWork;
    }
  } else {
    // There is no effect on the root.
    firstEffect = finishedWork.firstEffect;
  }

  if (firstEffect !== null) {
    ...
    nextEffect = firstEffect;

    do {
      {
        invokeGuardedCallback(null, commitBeforeMutationEffects, null);
        ...
      }
    } while (nextEffect !== null);

    stopCommitSnapshotEffectsTimer();
    ...
    startCommitHostEffectsTimer();
    nextEffect = firstEffect;

    do {
      {
        invokeGuardedCallback(null, commitMutationEffects, null, root, renderPriorityLevel);

        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);

    stopCommitHostEffectsTimer();
    ...
    startCommitLifeCyclesTimer();
    nextEffect = firstEffect;

    do {
      {
        invokeGuardedCallback(null, commitLayoutEffects, null, root, expirationTime);
        ...
      }
    } while (nextEffect !== null);

    stopCommitLifeCyclesTimer();
    nextEffect = null; // Tell Scheduler to yield at the end of the frame, so the browser has an
    // opportunity to paint.

    requestPaint();

    {
      popInteractions(prevInteractions);
    }

    executionContext = prevExecutionContext;
  } else {
    ...
  }

  stopCommitTimer();
  ...

  onCommitRoot(finishedWork.stateNode, expirationTime); // Always call this before exiting `commitRoot`, to ensure that any
  // additional work on this root is scheduled.

  ensureRootIsScheduled(root);


  flushSyncCallbackQueue();
  return null;
}

相关文章

网友评论

    本文标题:react浅析1 初始渲染

    本文链接:https://www.haomeiwen.com/subject/ezcdektx.html