DAY 1(React 17 源码,mount阶段的reconcile流程)

171 阅读9分钟

一、前言

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

二、DAY 1 目标

  1. mount阶段的renderRootSync流程
  2. 常用对象的数据结构
  3. 常用API罗列

三、mount阶段renderRootSync流程图

  1. 创建FiberRoot对象

  2. 更新Container

  3. 【调和阶段】根据FiberRoot创建整个Fiber链表

    • workInProgress指向给当前Fiber
    • 通过reconcileFhildren初始化子节点(为每一个子节点创建Fiber)
    • workInProgress指向下一个Fiber(有子节点则为第一个子节点,没有子节点则为兄弟节点)
    • 重复a ~ c 步骤,直到遍历完所有节点,workInProgress = null
  4. 【commit阶段】根据FiberRoot.firstEffect字段,进行真实DOM渲染

四、常用对象的数据结构

Fiber

根节点、组件、DOM元素、Text,都有一个与之对应的FiberNode

packages/react-reconciler/src/ReactInternalTypes.js

重要属性

  1. memoizedState 用于创建输出的状态
  2. pendingProps 新的变动带来的新的props,即nextProps
  3. stateNode 当前fiber的本地状态, 如果fiber对应的是class组件,则为class实例;如果是普通节点,则是 DOM元素;如果是函数组件,则为null
  4. alternate current fiber指向work in progress fiber;working in progress fiber指向current fiber
  5. updateQueue: 状态更新,回调和DOM更新的队列,Fiber对应的组件,所产生的update,都会放在该队列中。{shared: {pending: {}}}
  6. nextEffect: 链表格式,链接下一个有副作用的fiber
  7. firstEffect | lastEffect 当子节点有副作用时,标记这个字段
export type Fiber = {|
  // These first fields are conceptually members of an Instance. This used to
  // be split into a separate type and intersected with the other Fiber fields,
  // but until Flow fixes its intersection bugs, we've merged them into a
  // single type.

  // An Instance is shared between all versions of a component. We can easily
  // break this out into a separate object to avoid copying so much to the
  // alternate versions of the tree. We put this on a single object for now to
  // minimize the number of objects created during the initial render.

  // Tag identifying the type of fiber.
  tag: WorkTag,

  // Unique identifier of this child.
  key: null | string,

  // The value of element.type which is used to preserve the identity during
  // reconciliation of this child.
  elementType: any,

  // The resolved function/class/ associated with this fiber.
  type: any,

  // The local state associated with this fiber.
  stateNode: any,

  // Conceptual aliases
  // parent : Instance -> return The parent happens to be the same as the
  // return fiber since we've merged the fiber and instance.

  // Remaining fields belong to Fiber

  // The Fiber to return to after finishing processing this one.
  // This is effectively the parent, but there can be multiple parents (two)
  // so this is only the parent of the thing we're currently processing.
  // It is conceptually the same as the return address of a stack frame.
  return: Fiber | null,

  // Singly Linked List Tree Structure.
  child: Fiber | null,
  sibling: Fiber | null,
  index: number,

  // The ref last used to attach this node.
  // I'll avoid adding an owner field for prod and model that as functions.
  ref:
    | null
    | (((handle: mixed) => void) & {_stringRef: ?string, ...})
    | RefObject,

  // Input is the data coming into process this fiber. Arguments. Props.
  pendingProps: any, // This type will be more specific once we overload the tag.
  memoizedProps: any, // The props used to create the output.

  // A queue of state updates and callbacks.
  updateQueue: mixed,

  // The state used to create the output
  memoizedState: any,

  // Dependencies (contexts, events) for this fiber, if it has any
  dependencies: Dependencies | null,

  // Bitfield that describes properties about the fiber and its subtree. E.g.
  // the ConcurrentMode flag indicates whether the subtree should be async-by-
  // default. When a fiber is created, it inherits the mode of its
  // parent. Additional flags can be set at creation time, but after that the
  // value should remain unchanged throughout the fiber's lifetime, particularly
  // before its child fibers are created.
  mode: TypeOfMode,

  // Effect
  flags: Flags,
  subtreeFlags: Flags,
  deletions: Array<Fiber> | null,

  // Singly linked list fast path to the next fiber with side-effects.
  nextEffect: Fiber | null,

  // The first and last fiber with side-effect within this subtree. This allows
  // us to reuse a slice of the linked list when we reuse the work done within
  // this fiber.
  firstEffect: Fiber | null,
  lastEffect: Fiber | null,

  lanes: Lanes,
  childLanes: Lanes,

  // This is a pooled version of a Fiber. Every fiber that gets updated will
  // eventually have a pair. There are cases when we can clean up pairs to save
  // memory if we need to.
  alternate: Fiber | null,

  // Time spent rendering this Fiber and its descendants for the current update.
  // This tells us how well the tree makes use of sCU for memoization.
  // It is reset to 0 each time we render and only updated when we don't bailout.
  // This field is only set when the enableProfilerTimer flag is enabled.
  actualDuration?: number,

  // If the Fiber is currently active in the "render" phase,
  // This marks the time at which the work began.
  // This field is only set when the enableProfilerTimer flag is enabled.
  actualStartTime?: number,

  // Duration of the most recent render time for this Fiber.
  // This value is not updated when we bailout for memoization purposes.
  // This field is only set when the enableProfilerTimer flag is enabled.
  selfBaseDuration?: number,

  // Sum of base times for all descendants of this Fiber.
  // This value bubbles up during the "complete" phase.
  // This field is only set when the enableProfilerTimer flag is enabled.
  treeBaseDuration?: number,

  // Conceptual aliases
  // workInProgress : Fiber ->  alternate The alternate used for reuse happens
  // to be the same as work in progress.
  // __DEV__ only
  _debugID?: number,
  _debugSource?: Source | null,
  _debugOwner?: Fiber | null,
  _debugIsCurrentlyTiming?: boolean,
  _debugNeedsRemount?: boolean,

  // Used to verify that the order of hooks does not change between renders.
  _debugHookTypes?: Array<HookType> | null,
|};

FiberRoot

根节点对象

packages/react-reconciler/src/ReactInternalTypes.js

重要属性

  1. containerInfo 根节点对应的DOM元素
  2. current FiberRoot对应创建的fiber
export type FiberRoot = {
  ...BaseFiberRootProperties,
  ...ProfilingOnlyFiberRootProperties,
  ...SuspenseCallbackOnlyFiberRootProperties
};

type BaseFiberRootProperties = {|
  // The type of root (legacy, batched, concurrent, etc.)
  tag: RootTag,

  // Any additional information from the host associated with this root.
  containerInfo: any,
  // Used only by persistent updates.
  pendingChildren: any,
  // The currently active root fiber. This is the mutable root of the tree.
  current: Fiber,

  pingCache: WeakMap<Wakeable, Set<mixed>> | Map<Wakeable, Set<mixed>> | null,

  // A finished work-in-progress HostRoot that's ready to be committed.
  finishedWork: Fiber | null,
  // Timeout handle returned by setTimeout. Used to cancel a pending timeout, if
  // it's superseded by a new one.
  timeoutHandle: TimeoutHandle | NoTimeout,
  // Top context object, used by renderSubtreeIntoContainer
  context: Object | null,
  pendingContext: Object | null,
  // Determines if we should attempt to hydrate on the initial mount
  +hydrate: boolean,

  // Used by useMutableSource hook to avoid tearing during hydration.
  mutableSourceEagerHydrationData?: Array<
    MutableSource<any> | MutableSourceVersion,
  > | null,

  // Node returned by Scheduler.scheduleCallback. Represents the next rendering
  // task that the root will work on.
  callbackNode: *,
  callbackPriority: LanePriority,
  eventTimes: LaneMap<number>,
  expirationTimes: LaneMap<number>,

  pendingLanes: Lanes,
  suspendedLanes: Lanes,
  pingedLanes: Lanes,
  expiredLanes: Lanes,
  mutableReadLanes: Lanes,

  finishedLanes: Lanes,

  entangledLanes: Lanes,
  entanglements: LaneMap<Lanes>,
|};

type ProfilingOnlyFiberRootProperties = {|
  interactionThreadID: number,
  memoizedInteractions: Set<Interaction>,
  pendingInteractionMap: Map<Lane | Lanes, Set<Interaction>>,
|};

export type SuspenseHydrationCallbacks = {
  onHydrated?: (suspenseInstance: SuspenseInstance) => void,
  onDeleted?: (suspenseInstance: SuspenseInstance) => void,
  ...
};

ReactElement

通过react.createElement方法调用后产生的element对象。可以描述组件和DOM元素

packages/shared/ReactElementType.js

重要属性

  1. type 描述组件时,存储组件函数;描述DOM元素时,存储DOM标签名
  2. props 描述element的节点属性,以及element的children
type ReactElement = {|
  $$typeof: any,
  type: any,
  key: any,
  ref: any,
  props: any,
  // ReactFiber
  _owner: any,

  // __DEV__
  _store: {validated: boolean, ...},
  _self: React$Element<any>,
  _shadowChildren: any,
  _source: Source,
|};

五、mount阶段reconcile关键API

Demo与编译后代码对比

JSX是react.createElement的语法糖

Demo-JSX

class SupComp extends React.Component{
  render() {
      return (
          <div>
              <p onClick={this.clickDiv}>sup</p>
              <p onClick={this.clickDiv2}>sup2{this.state.param2}</p>
              <div>last node {this.state.isParent.toString()}</div>
          </div>
      )
  }
}

ReactDOM.render(<SupComp />, mountNode);

编译后-JSX

class SupComp extends React.Component{
  render() {
    return react.createElement("div", {
      __source: {
        fileName: _jsxFileName,
        lineNumber: 195
      },
      __self: this
    }, react.createElement("p", {
      onClick: this.clickDiv,
      __source: {
        fileName: _jsxFileName,
        lineNumber: 196
      },
      __self: this
    }, "sup"), react.createElement("p", {
      onClick: this.clickDiv2,
      __source: {
        fileName: _jsxFileName,
        lineNumber: 197
      },
      __self: this
    }, "sup2", this.state.param2), react.createElement("div", {
      __source: {
        fileName: _jsxFileName,
        lineNumber: 208
      },
      __self: this
    }, "last node ", this.state.isParent.toString()));
  }
}

reactdom.render(react.createElement(SupComp, {
  __source: {
    fileName: _jsxFileName,
    lineNumber: 214
  },
  __self: undefined
}), mountNode);

React.createElement

packages/react/src/ReactElement.js

关键功能

  1. 定义props对象,把config里的属性,children,type.defaultProps依次放到props里
  2. 通过ReactElement方法,生成ReactElement对象,并返回

主要代码

function createElement(type, config, children) {
  let propName;

  // Reserved names are extracted
  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  if (config != null) {
    // Remaining properties are added to a new props object
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // Children can be more than one argument, and those are transferred onto
  // the newly allocated props object.
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  // Resolve default props
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

performUnitOfWork

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

关键功能

  1. 初始化或更新workInProgress。
  2. 同时workInProgress指向下一个Fiber
    1. Fiber指向第一个子节点
    2. Fiber指向相邻节点
    3. Fiber指向null【workInProgress Fiber链表已执行完毕,去commit阶段】

主要代码

function performUnitOfWork(unitOfWork: Fiber): void {
  // 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.
  const current = unitOfWork.alternate;
  setCurrentDebugFiberInDEV(unitOfWork);

  let next;
  next = beginWork(current, unitOfWork, subtreeRenderLanes);

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

  ReactCurrentOwner.current = null;
}

beginWork

packages/react-reconciler/src/ReactFiberBeginWork.old.js

关键功能

根据不同的workInProgress.tag,进行不同的逻辑处理。

其中,最常用的是:

  1. updateHostRoot(根节点处理)
  2. updateClassComponent(class组件处理)
  3. mountIndeterminateComponent(函数组件初始入口,function只有在返回的时候才知道是函数组件,因此都会统一进入到这里处理)
  4. updateHostComponent(原生DOM节点处理)
  5. updateHostText(原生Text节点处理)

不同执行过程中,会对Fiber里的不同字段进行各自的处理。

而这些方法的共同功能是:

  1. 判断当前Fiber是否有子节点,如果有,则通过reconcileChildren方法进行对子节点挨个初始化(新建Fiber)。
  2. 最终用Fiber的child和return字段把父节点和第一个子节点互相关联起来(伪代码可以看下面的reconcileChildren部分)。
  3. 返回第一个子节点

主要代码

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null { 
  // ... 
  
  workInProgress.lanes = NoLanes;

  switch (workInProgress.tag) {
    case IndeterminateComponent: {
      return mountIndeterminateComponent(
        current,
        workInProgress,
        workInProgress.type,
        renderLanes,
      );
    }
    case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case ClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText:
      return updateHostText(current, workInProgress);
    // ... case more
  }
}

completeUnitOfWork

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

关键功能

当WorkInProgress没有子节点的时候,会进入到completeWork方法。

completeWork方法,主要提供了如下功能(completedWork:当前Fiber节点;returnFiber:父Fiber节点):

  1. 对completedWork执行completeWork方法,完成最终的FIber属性计算和处理
  2. 通过completedWork的firstEffect和lastEffect对returnFiber的firstEffect和lastEffect进行赋值处理(并一层层向上传递)
  3. 如果completedWork需要更新,则赋值returnFiber.firstEffect = completedWork
  4. 如果completedWork有兄弟节点,则workInProgress = siblingFiber
  5. 如果completedWork没有兄弟节点,则执行 completedWork = returnFiber 和 workInProgress = completedWork
  6. 重复执行1~5步骤,直至 completedWork 为null(遍历完了workInProgress Fiber链表的所有节点)

\

主要代码

function completeUnitOfWork(unitOfWork: Fiber): void {
  // Attempt to complete the current unit of work, then move to the next
  // sibling. If there are no more siblings, return to the parent fiber.
  let completedWork = unitOfWork;
  do {
    // 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.
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;

    // Check if the work completed or if something threw.
    if ((completedWork.flags & Incomplete) === NoFlags) {
      next = completeWork(current, completedWork, subtreeRenderLanes);

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

      if (
        returnFiber !== null &&
        // Do not append effects to parents if a sibling failed to complete
        (returnFiber.flags & Incomplete) === NoFlags
      ) {
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = completedWork.firstEffect;
        }
        if (completedWork.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
          }
          returnFiber.lastEffect = completedWork.lastEffect;
        }

        const flags = completedWork.flags;
        if (flags > PerformedWork) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork;
          } else {
            returnFiber.firstEffect = completedWork;
          }
          returnFiber.lastEffect = completedWork;
        }
      }
    } else {
      // This fiber did not complete because something threw. Pop values off
      // the stack without entering the complete phase. If this is a boundary,
      // capture values if possible.
    }

    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      workInProgress = siblingFiber;
      return;
    }
    // Otherwise, return to the parent
    completedWork = returnFiber;
    // Update the next thing we're working on in case something throws.
    workInProgress = completedWork;
  } while (completedWork !== null);
}

reconcileChildren / reconcileChildFibers

packages/react-reconciler/src/ReactChildFiber.old.js

关键功能

reconcileChildren只是对reconcileChildFibers进行简单的包装。主要处理在reconcileChildFibers。

reconcileChildFibers方法对workInProgress Fiber的子节点进行初始化并根据child类型(Object/String/Number/Array)创建对应Fiber。并返回第一个子节点Fiber。

\

总结

只要搞清楚以下3点,那么对理解React的mount阶段有很大帮助

  1. 创建完根节点(FiberRoot和对于的Fiber)后,通过workLoopSync方法,循环创建workInProgress Fiber链表。
  2. performUnitOfWork方法如何去执行并创建Fiber,以及workInProgress对象在不同情况下的赋值。
  3. 在创建workInProgress Fiber时,如果没有子节点,那么通过completeUnitOfWork方法去寻找自己的兄弟节点(或父节点的兄弟节点)。如果没有找到兄弟节点,则返回到根节点,并赋值workInProgress为null。结束reconcile阶段,进入commit阶段。