DAY 2(React 17 源码,mount阶段的commit流程和setState过程)

258 阅读5分钟

一、前言

最近的几次面试都被问到了框架原理。记忆力不太行的我往往背诵完后很快就会忘记,因此打算花21个小时(分3天,每天7小时)去好好的阅读下React框架,了解其中几个关键功能点的执行逻辑。
框架版本:React 17,enableNewReconciler=false
学习方式:通过debug一步步调试JS代码的方式,来深入了解React框架的主流程
文中的代码片段,只保留作者认为重要的代码

二、DAY 2 目标

  1. mount阶段的commit流程
  2. setState过程
  3. setState后renderRootSync和commitRoot的不同

三、mount阶段的commit流程图

只展示主流程,以及伪代码

四、setState过程

react17中:只有在setTimeout、DOM原生事件绑定等React不能管控的方法之内调用setState是同步的,其他情况setState都是异步的。

setState流程

调用组件的enqueueSetState方法

enqueueSetState = function(inst, payload, callback) {
  const fiber = getInstance(inst);
  const eventTime = requestEventTime();
  const lane = requestUpdateLane(fiber);

  const update = createUpdate(eventTime, lane);
  update.payload = payload;
  if (callback !== undefined && callback !== null) {
    update.callback = callback;
  }
  // 更新fiber的updateQueue(把newState赋值给相关字段)
  enqueueUpdate(fiber, update);
  scheduleUpdateOnFiber(fiber, lane, eventTime);

  if (enableSchedulingProfiler) {
    markStateUpdateScheduled(fiber, lane);
  }
}

调用enqueueSetState方法

把newState放到class组件对应Fiber的updateQueue.shared.pending字段里,以便在调和阶段里取用

export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
  const updateQueue = fiber.updateQueue;
  if (updateQueue === null) {
    // Only occurs if the fiber has been unmounted.
    return;
  }

  const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
  const 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;
}

调用scheduleUpdateOnFiber

通过ensureRootIsScheduled方法更新syncQueue队列,
并通过executionContext判断是否立即进入调和阶段重新render。

function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  const priorityLevel = getCurrentPriorityLevel();

  if (lane === SyncLane) {
    ensureRootIsScheduled(root, eventTime);
    schedulePendingInteractions(root, lane);
    if (executionContext === NoContext) {
      resetRenderTimer();
      // 同步执行
      flushSyncCallbackQueue();
    }
  }
  mostRecentlyUpdatedRoot = root;
}

React17 中如何实现异步

如上一点所示,对于判断是否是异步最关键的参数是executionContext字段。

所以,在正常调用setState过程中,有地方会对executionContext进行赋值,使它不等于NoContext。从而进入异步过程。

具体实现则在React的合成事件中。

合成事件

React通过“根节点事件代理”、“事件分发”、“原生事件对象封装”实现了“合成事件”。【网络上有很多关于合成事件的文章】。在具体执行用户绑定的事件回调方法前,其实已经在React内部执行了一系列的方法(如下图)。

try/final 和 事务机制

上图中,从其中的几个方法【discreteUpdates、batchedEventUpdates】里,可以清晰的看到,通过try/final 方法,在执行具体代码前后,都对executionContext进行了操作。所以当最外层事件触发方法的final方法时(此时executionContext === NoContext),执行flushSyncCallbackQueue方法,处理syncQueue队列里的回调,完成组件的re-render。

function discreteUpdates$1(fn, a, b, c, d) {
  // 此时executionContext === 0
  var prevExecutionContext = executionContext;
  executionContext |= DiscreteEventContext;

  {
    try {
      return runWithPriority$1(UserBlockingPriority$2, fn.bind(null, a, b, c, d));
    } finally {
      executionContext = prevExecutionContext;

      if (executionContext === NoContext) {
        // Flush the immediate callbacks that were scheduled during this batch
        resetRenderTimer();
        // 清空syncQueue队列
        flushSyncCallbackQueue();
      }
    }
  }
}

五、re-render阶段renderRootSync和commitRoot的不同

当组件需要重新渲染时,大部分的逻辑同mount阶段是一致的。

其中最大的区别在于:

  1. 优先通过current Fiber.alternate创建workInProgress Fiber
  2. 在reconcileChildren时,会有diff current和workInProgress的逻辑

renderRootSync

复用current Fiber

function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode,
    );
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;

    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    workInProgress.pendingProps = pendingProps;
    workInProgress.type = current.type;

    workInProgress.flags = NoFlags;

    workInProgress.nextEffect = null;
    workInProgress.firstEffect = null;
    workInProgress.lastEffect = null;
  }

  workInProgress.childLanes = current.childLanes;
  workInProgress.lanes = current.lanes;

  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;

  const currentDependencies = current.dependencies;
  workInProgress.dependencies =
    currentDependencies === null
      ? null
      : {
          lanes: currentDependencies.lanes,
          firstContext: currentDependencies.firstContext,
        };

  // These will be overridden during the parent's reconciliation
  workInProgress.sibling = current.sibling;
  workInProgress.index = current.index;
  workInProgress.ref = current.ref;

  return workInProgress;
}

diff节点

reconcileSingleElement

通过child.key === key和child.elementType === element.type,来判断是否是同一个节点。

是同一个节点,则复用current Fiber,

不是同一个节点,则通过deleteRemainingChildren删除所有的children节点,并在之后通过createFiberFromElement方法创建新的workInProgress Fiber

function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;
    while (child !== null) {
      if (child.key === key) {
        switch (child.tag) {
          case Block:
            if (enableBlocksAPI) {
              let type = element.type;
              if (type.$$typeof === REACT_BLOCK_TYPE) {
                // The new Block might not be initialized yet. We need to initialize
                // it in case initializing it turns out it would match.
                if (
                  ((type: any): BlockComponent<any, any>)._render ===
                  (child.type: BlockComponent<any, any>)._render
                ) {
                  deleteRemainingChildren(returnFiber, child.sibling);
                  const existing = useFiber(child, element.props);
                  existing.type = type;
                  existing.return = returnFiber;
                  if (__DEV__) {
                    existing._debugSource = element._source;
                    existing._debugOwner = element._owner;
                  }
                  return existing;
                }
              }
            }
          // We intentionally fallthrough here if enableBlocksAPI is not on.
          // eslint-disable-next-lined no-fallthrough
          default: {
            if (
              child.elementType === element.type
            ) {
              deleteRemainingChildren(returnFiber, child.sibling);
              const existing = useFiber(child, element.props);
              existing.ref = coerceRef(returnFiber, child, element);
              existing.return = returnFiber;
              return existing;
            }
            break;
          }
        }
        // Didn't match.
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }

    if (element.type === REACT_FRAGMENT_TYPE) {
      const created = createFiberFromFragment(
        element.props.children,
        returnFiber.mode,
        lanes,
        element.key,
      );
      created.return = returnFiber;
      return created;
    } else {
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      created.return = returnFiber;
      return created;
    }
  }

reconcileChildrenArray

childrenArray的diff逻辑,最主要是在2个for循环

循环1

依次对新旧节点进行比较。

在当前循环中,在updateSlot方法里判断是否是同一个节点,并返回新节点的Fiber。

  1. 如果newFiber和oldFiber都为null,则对比下一个兄弟节点
  2. 如果oldFiber存在,且newFiber是新建的(不复用oldFiber),表示不是同一个节点,则删除oldFiber
  3. 如果newFiber和oldFiber是同一个节点,则对比下一个兄弟节点(同时对newFiber通过next形成链表)
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
  if (oldFiber.index > newIdx) {
    nextOldFiber = oldFiber;
    oldFiber = null;
  } else {
    nextOldFiber = oldFiber.sibling;
  }
  // 新建workInProgress Fiber
  const newFiber = updateSlot(
    returnFiber,
    oldFiber,
    newChildren[newIdx],
    lanes,
  );
  if (newFiber === null) {
    if (oldFiber === null) {
      oldFiber = nextOldFiber;
    }
    break;
  }
  if (shouldTrackSideEffects) {
    if (oldFiber && newFiber.alternate === null) {
      deleteChild(returnFiber, oldFiber);
    }
  }
  lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
  // 对所有的newFiber形成链表
  if (previousNewFiber === null) {
    resultingFirstChild = newFiber;
  } else {
    previousNewFiber.sibling = newFiber;
  }
  previousNewFiber = newFiber;
  oldFiber = nextOldFiber;
}
循环2

当还有新节点没创建时触发。

通过createChild创建Fiber,并插入到之前新建Fiber链表的尾部

for (; newIdx < newChildren.length; newIdx++) {
  const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
  if (newFiber === null) {
    continue;
  }
  lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
  if (previousNewFiber === null) {
    // TODO: Move out of the loop. This only happens for the first run.
    resultingFirstChild = newFiber;
  } else {
    previousNewFiber.sibling = newFiber;
  }
  previousNewFiber = newFiber;
}
完整代码
function reconcileChildrenArray(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChildren: Array<*>,
  lanes: Lanes,
): Fiber | null {
  let resultingFirstChild: Fiber | null = null;
  let previousNewFiber: Fiber | null = null;

  let oldFiber = currentFirstChild;
  let lastPlacedIndex = 0;
  let newIdx = 0;
  let nextOldFiber = null;
  // 循环1
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    if (oldFiber.index > newIdx) {
      nextOldFiber = oldFiber;
      oldFiber = null;
    } else {
      nextOldFiber = oldFiber.sibling;
    }
    const newFiber = updateSlot(
      returnFiber,
      oldFiber,
      newChildren[newIdx],
      lanes,
    );
    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;
  }
  // 循环2
  if (oldFiber === null) {
    // If we don't have any more existing children we can choose a fast path
    // since the rest will all be insertions.
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
      if (newFiber === null) {
        continue;
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        // TODO: Move out of the loop. This only happens for the first run.
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
    return resultingFirstChild;
  }

  // Add all children to a key map for quick lookups.
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

  // Keep scanning and use the map to restore deleted items as moves.
  for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = updateFromMap(
      existingChildren,
      returnFiber,
      newIdx,
      newChildren[newIdx],
      lanes,
    );
    if (newFiber !== null) {
      if (shouldTrackSideEffects) {
        if (newFiber.alternate !== null) {
          // The new fiber is a work in progress, but if there exists a
          // current, that means that we reused the fiber. We need to delete
          // it from the child list so that we don't add it to the deletion
          // list.
          existingChildren.delete(
            newFiber.key === null ? newIdx : newFiber.key,
          );
        }
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
  }

  if (shouldTrackSideEffects) {
    // Any existing children that weren't consumed above were deleted. We need
    // to add them to the deletion list.
    existingChildren.forEach(child => deleteChild(returnFiber, child));
  }

  return resultingFirstChild;
}

commitRoot

commit阶段,最大的区别是在root.finishedWork。

mount阶段时,finishedWork指向根节点的child。而re-render阶段,finishedWork指向的是需要更新的Fiber,如果有多个Fiber需要更新,则通过nextEffect形成链表。

总结

  1. commit阶段,通过在reconcile阶段生成的root.finishedWork.firstEffect,通过commitMutationEffects方法一次对需要更新的Fiber进行DOM渲染。
  2. setState通过事务机制对executionContext进行赋值,并通过executionContext === NoContext 来判断当前是进行异步还是同步。
  3. diff逻辑
    • 只比较同一层的节点。
    • 通过key和elementType判断是否是同一个节点。不是则删除老节点新建新节点,是则复用老节点。
    • diff多个子节点时,通过for循环从头至尾逐一进行比较(不像vue那样通过双指针来优化diff逻辑)。