React原始碼閱讀:虛擬DOM的初始化

後排的風過發表於2019-03-02

前言

本文的主要目的是閱讀原始碼的過程中做下筆記和分享給有需要的小夥伴,可能會有紕漏和錯誤,請讀者自行判斷,頭一次寫閱讀程式碼的文章,可能寫得有點亂,有什麼問題歡迎一起探討一起進步。

React的版本為16.4,主分支的程式碼,只貼出部分關鍵程式碼,完整程式碼請到Github檢視。

React閱讀系列文章

React原始碼閱讀:概況

虛擬DOM的初始化

React.createElement

在閱讀原始碼前,我們先提出一個問題,React是如何將虛擬DOM轉換為真實的DOM呢?有問題以後我們才會更有目標的閱讀程式碼,下面我們就帶著這個問題去思考。

在平時工作中我們常常用JSX語法來建立React元素例項,但他們最後都會通過打包工具編譯成原生的JS程式碼,通過React.createElement來建立。例如:

//  class ReactComponent extends React.Component {
//      render() {
//          return <p className="class">Hello React</p>;
//      }
//  }
//  以上程式碼會編譯為:
class ReactComponent extends React.Component {
    render() {
        React.createElement(
          `p`,
          { className: `class`},
          `Hello React`
        )
    }
}

//  <ReactComponent someProp="prop" />
React.createElement(ReactComponent, { someProp: `prop` }, null);

複製程式碼

這樣我們就可以建立得到React元素例項。
先來看看createElement的主要原始碼(部分程式碼省略):

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

  const props = {};

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

  if (config != null) {
    if (hasValidRef(config)) {
      //  如果有ref,將他取出來
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      //  如果有key,將他取出來
      key = `` + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        //  將除ref,key等這些特殊的屬性放到新的props物件裡
        props[propName] = config[propName];
      }
    }
  }

  //  獲取子元素
  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;
  }

  //  新增預設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,
  );
}
複製程式碼
const ReactElement = function(type, key, ref, self, source, owner, props) {
  //  最終得到的React元素
  const element = {
    // This tag allows us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner,
  };

  return element;
};
複製程式碼

是不是很簡單呢,主要是把我們傳進去的東西組成一個React元素物件,而type就是我們傳進去的元件型別,他可以是一個類、函式或字串(如`div`)。

ReactDom.render

雖然我們已經得到建立好的React元素,但React有是如何把React元素轉換為我們最終想要的DOM呢?就是通過ReactDom.render函式啦。

ReactDom.render(
  React.createElement(App),  
  document.getElementById(`root`)
 );
複製程式碼

ReactDom.render的定義:

render(
    element: React$Element<any>,
    container: DOMContainer,
    callback: ?Function,
  ) {
    return legacyRenderSubtreeIntoContainer(
      null,  /* 父元件 */
      element,  /* React元素 */
      container,  /* DOM容器 */
      false,
      callback,
    );
  }
複製程式碼

legacyRenderSubtreeIntoContainer先獲取到React根容器物件(只貼部分程式碼):

...
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
  container,
  forceHydrate,
);
...
複製程式碼

因程式碼過多隻貼出通過legacyCreateRootFromDOMContainer最終得到的React根容器物件:

const NoWork = 0;

{
    _internalRoot: {
      current: uninitializedFiber,  // null
      containerInfo: containerInfo,  //  DOM容器
      pendingChildren: null,

      earliestPendingTime: NoWork,
      latestPendingTime: NoWork,
      earliestSuspendedTime: NoWork,
      latestSuspendedTime: NoWork,
      latestPingedTime: NoWork,

      didError: false,

      pendingCommitExpirationTime: NoWork,
      finishedWork: null,
      context: null,
      pendingContext: null,
      hydrate,
      nextExpirationTimeToWorkOn: NoWork,
      expirationTime: NoWork,
      firstBatch: null,
      nextScheduledRoot: null,
    },
    render: (children: ReactNodeList, callback: ?() => mixed) => Work,
    legacy_renderSubtreeIntoContainer: (
      parentComponent: ?React$Component<any, any>,
      children: ReactNodeList,
      callback: ?() => mixed
    ) => Work,
    createBatch: () => Batch
}
複製程式碼

在初始化React根容器物件root後,呼叫root.render開始虛擬DOM的渲染過程。

DOMRenderer.unbatchedUpdates(() => {
  if (parentComponent != null) {
    root.legacy_renderSubtreeIntoContainer(
      parentComponent,
      children,
      callback,
    );
  } else {
    root.render(children, callback);
  }
});
複製程式碼

因程式碼量過大,就不逐一貼出詳細程式碼,只貼出主要的函式的呼叫過程。

root.render(children, callback) -> 
DOMRenderer.updateContainer(children, root, null, work._onCommit) -> 
updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    callback,
) ->
scheduleRootUpdate(current, element, expirationTime, callback) ->
scheduleWork(current, expirationTime) ->
requestWork(root, rootExpirationTime) ->
performWorkOnRoot(root, Sync, false) ->
renderRoot(root, false) -> 
workLoop(isYieldy) ->
performUnitOfWork(nextUnitOfWork: Fiber) => Fiber | null ->
beginWork(current, workInProgress, nextRenderExpirationTime)
複製程式碼

Fiber

Fiber型別:

type Fiber = {|
  tag: TypeOfWork,
  key: null | string,

  // The function/class/module associated with this fiber.
  type: any,
  return: Fiber | null,

  // Singly Linked List Tree Structure.
  child: Fiber | null,
  sibling: Fiber | null,
  index: number,
  ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,

  memoizedProps: any, // The props used to create the output.
  updateQueue: UpdateQueue<any> | null,
  memoizedState: any,
  
  mode: TypeOfMode,

  effectTag: TypeOfSideEffect,
  nextEffect: Fiber | null,
  firstEffect: Fiber | null,
  lastEffect: Fiber | null,
  
  expirationTime: ExpirationTime,

  alternate: Fiber | null,

  actualDuration?: number,
  actualStartTime?: number,
  selfBaseTime?: number,
  treeBaseTime?: number,

  _debugID?: number,
  _debugSource?: Source | null,
  _debugOwner?: Fiber | null,
  _debugIsCurrentlyTiming?: boolean,
|};
複製程式碼

beginWork

按以上函式呼叫過程,我們來到beginWork函式,它的作用主要是根據Fiber物件的tag來對元件進行mount或update:

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
): Fiber | null {
  if (enableProfilerTimer) {
    if (workInProgress.mode & ProfileMode) {
      markActualRenderTimeStarted(workInProgress);
    }
  }

  if (
    workInProgress.expirationTime === NoWork ||
    workInProgress.expirationTime > renderExpirationTime
  ) {
    return bailoutOnLowPriority(current, workInProgress);
  }

  //  根據元件型別來進行不同處理
  switch (workInProgress.tag) {
    case IndeterminateComponent:
      //  不確定的元件型別
      return mountIndeterminateComponent(
        current,
        workInProgress,
        renderExpirationTime,
      );
    case FunctionalComponent:
      //  函式型別的元件
      return updateFunctionalComponent(current, workInProgress);
    case ClassComponent:
      //  類型別的元件,我們這次主要看這個
      return updateClassComponent(
        current,
        workInProgress,
        renderExpirationTime,
      );
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderExpirationTime);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderExpirationTime);
    case HostText:
      return updateHostText(current, workInProgress);
    case TimeoutComponent:
      return updateTimeoutComponent(
        current,
        workInProgress,
        renderExpirationTime,
      );
    case HostPortal:
      return updatePortalComponent(
        current,
        workInProgress,
        renderExpirationTime,
      );
    case ForwardRef:
      return updateForwardRef(current, workInProgress);
    case Fragment:
      return updateFragment(current, workInProgress);
    case Mode:
      return updateMode(current, workInProgress);
    case Profiler:
      return updateProfiler(current, workInProgress);
    case ContextProvider:
      return updateContextProvider(
        current,
        workInProgress,
        renderExpirationTime,
      );
    case ContextConsumer:
      return updateContextConsumer(
        current,
        workInProgress,
        renderExpirationTime,
      );
    default:
      invariant(
        false,
        `Unknown unit of work tag. This error is likely caused by a bug in ` +
          `React. Please file an issue.`,
      );
  }
}
複製程式碼

updateClassComponent

updateClassComponent的作用是對未初始化的類元件進行初始化,對已經初始化的元件更新重用

function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
) {
  const hasContext = pushLegacyContextProvider(workInProgress);
  let shouldUpdate;
  if (current === null) {
    if (workInProgress.stateNode === null) {
      //  如果還沒建立例項,初始化
      constructClassInstance(
        workInProgress,
        workInProgress.pendingProps,
        renderExpirationTime,
      );
      mountClassInstance(workInProgress, renderExpirationTime);

      shouldUpdate = true;
    } else {
      //  如果已經建立例項,則重用例項
      shouldUpdate = resumeMountClassInstance(
        workInProgress,
        renderExpirationTime,
      );
    }
  } else {
    shouldUpdate = updateClassInstance(
      current,
      workInProgress,
      renderExpirationTime,
    );
  }
  return finishClassComponent(
    current,
    workInProgress,
    shouldUpdate,
    hasContext,
    renderExpirationTime,
  );
}
複製程式碼

constructClassInstance

例項化類元件:

function constructClassInstance(
  workInProgress: Fiber,
  props: any,
  renderExpirationTime: ExpirationTime,
): any {
  const ctor = workInProgress.type;  //  我們傳進去的那個類
  const unmaskedContext = getUnmaskedContext(workInProgress);
  const needsContext = isContextConsumer(workInProgress);
  const context = needsContext
    ? getMaskedContext(workInProgress, unmaskedContext)
    : emptyContextObject;

  const instance = new ctor(props, context);  //  建立例項
  const state = (workInProgress.memoizedState =
    instance.state !== null && instance.state !== undefined
      ? instance.state
      : null);
  adoptClassInstance(workInProgress, instance);

  if (needsContext) {
    cacheContext(workInProgress, unmaskedContext, context);
  }

  return instance;
}
複製程式碼

adoptClassInstance

function adoptClassInstance(workInProgress: Fiber, instance: any): void {
  instance.updater = classComponentUpdater;
  workInProgress.stateNode = instance;  //  將例項賦值給stateNode屬性
}
複製程式碼

mountClassInstance

下面的程式碼就有我們熟悉的componentWillMount生命週期出現啦,不過新版React已經不建議使用它。

function mountClassInstance(
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
): void {
  const ctor = workInProgress.type;

  const instance = workInProgress.stateNode;
  const props = workInProgress.pendingProps;
  const unmaskedContext = getUnmaskedContext(workInProgress);

  instance.props = props;
  instance.state = workInProgress.memoizedState;
  instance.refs = emptyRefsObject;
  instance.context = getMaskedContext(workInProgress, unmaskedContext);

  let updateQueue = workInProgress.updateQueue;
  if (updateQueue !== null) {
    processUpdateQueue(
      workInProgress,
      updateQueue,
      props,
      instance,
      renderExpirationTime,
    );
    instance.state = workInProgress.memoizedState;
  }

  const getDerivedStateFromProps = workInProgress.type.getDerivedStateFromProps;
  if (typeof getDerivedStateFromProps === `function`) {
    //  React新的生命週期函式
    applyDerivedStateFromProps(workInProgress, getDerivedStateFromProps, props);
    instance.state = workInProgress.memoizedState;
  }

  if (
    typeof ctor.getDerivedStateFromProps !== `function` &&
    typeof instance.getSnapshotBeforeUpdate !== `function` &&
    (typeof instance.UNSAFE_componentWillMount === `function` ||
      typeof instance.componentWillMount === `function`)
  ) {
    //  如果沒有使用getDerivedStateFromProps而使用componentWillMount,相容舊版
    callComponentWillMount(workInProgress, instance);
    updateQueue = workInProgress.updateQueue;
    if (updateQueue !== null) {
      processUpdateQueue(
        workInProgress,
        updateQueue,
        props,
        instance,
        renderExpirationTime,
      );
      instance.state = workInProgress.memoizedState;
    }
  }

  if (typeof instance.componentDidMount === `function`) {
    workInProgress.effectTag |= Update;
  }
}
複製程式碼

finishClassComponent

呼叫元件例項的render函式獲取需渲染的子元素,並把子元素進行處理為Fiber型別,處理state和props:

function finishClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  shouldUpdate: boolean,
  hasContext: boolean,
  renderExpirationTime: ExpirationTime,
) {
  markRef(current, workInProgress);

  const didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect;

  if (!shouldUpdate && !didCaptureError) {
    if (hasContext) {
      invalidateContextProvider(workInProgress, false);
    }

    return bailoutOnAlreadyFinishedWork(current, workInProgress);
  }

  const ctor = workInProgress.type;
  const instance = workInProgress.stateNode;

  ReactCurrentOwner.current = workInProgress;
  let nextChildren;
  if (
    didCaptureError &&
    (!enableGetDerivedStateFromCatch ||
      typeof ctor.getDerivedStateFromCatch !== `function`)
  ) {
    nextChildren = null;

    if (enableProfilerTimer) {
      stopBaseRenderTimerIfRunning();
    }
  } else {
    if (__DEV__) {
      ...
    } else {
    //  呼叫render函式獲取子元素
      nextChildren = instance.render();
    }
  }

  workInProgress.effectTag |= PerformedWork;
  if (didCaptureError) {
    reconcileChildrenAtExpirationTime(
      current,
      workInProgress,
      null,
      renderExpirationTime,
    );
    workInProgress.child = null;
  }
  //  把子元素轉換為Fiber型別
  //  如果子元素數量大於一(即為陣列)的時候,
  //  返回第一個Fiber型別子元素
  reconcileChildrenAtExpirationTime(
    current,
    workInProgress,
    nextChildren,
    renderExpirationTime,
  );
  //  處理state
  memoizeState(workInProgress, instance.state);
  //  處理props
  memoizeProps(workInProgress, instance.props);

  if (hasContext) {
    invalidateContextProvider(workInProgress, true);
  }

  //  返回Fiber型別的子元素給beginWork函式,
  //  一直返回到workLoop函式(看上面的呼叫過程)
  return workInProgress.child;
}
複製程式碼

workLoop

我們再回頭看下workLoop和performUnitOfWork函式(看上面的函式呼叫過程),當benginWork對不同型別元件完成相應處理返回子元素後,workLoop繼續通過performUnitOfWork來呼叫benginWork對子元素進行處理,從而遍歷虛擬DOM樹:

function workLoop(isYieldy) {
  if (!isYieldy) {
    while (nextUnitOfWork !== null) {
      //  遍歷整棵虛擬DOM樹
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  } else {
    while (nextUnitOfWork !== null && !shouldYield()) {
      //  遍歷整棵虛擬DOM樹
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }

    if (enableProfilerTimer) {
      pauseActualRenderTimerIfRunning();
    }
  }
}
複製程式碼

performUnitOfWork

這個函式很接近把虛擬DOM轉換為真實DOM的程式碼啦,當遍歷完成一顆虛擬DOM的子樹後(beginWork返回null,即已沒有子元素),呼叫completeUnitOfWork函式開始轉換:

function performUnitOfWork(workInProgress: Fiber): Fiber | null {
  const current = workInProgress.alternate;

  startWorkTimer(workInProgress);

  let next;
  if (enableProfilerTimer) {
    if (workInProgress.mode & ProfileMode) {
      startBaseRenderTimer();
    }

    next = beginWork(current, workInProgress, nextRenderExpirationTime);

    if (workInProgress.mode & ProfileMode) {
      recordElapsedBaseRenderTimeIfRunning(workInProgress);
      stopBaseRenderTimerIfRunning();
    }
  } else {
    next = beginWork(current, workInProgress, nextRenderExpirationTime);
  }

  if (next === null) {
    next = completeUnitOfWork(workInProgress);
  }

  ReactCurrentOwner.current = null;

  return next;
}
複製程式碼

completeUnitOfWork

此函式作用為先將當前Fiber元素轉換為真實DOM節點,然後在看有無兄弟節點,若有則返回給上層函式處理完後再呼叫此函式進行轉換;否則檢視有無父節點,若有則轉換父節點。

function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
  while (true) {
    const current = workInProgress.alternate;

    const returnFiber = workInProgress.return;
    const siblingFiber = workInProgress.sibling;

    if ((workInProgress.effectTag & Incomplete) === NoEffect) {
      //  呼叫completeWork轉換虛擬DOM
      let next = completeWork(
        current,
        workInProgress,
        nextRenderExpirationTime,
      );
      stopWorkTimer(workInProgress);
      resetExpirationTime(workInProgress, nextRenderExpirationTime);

      if (next !== null) {
        stopWorkTimer(workInProgress);
        return next;
      }

      //  處理完當前節點後
      if (siblingFiber !== null) {
        //  如果有兄弟節點,則將其返回
        return siblingFiber;
      } else if (returnFiber !== null) {
        //  沒有兄弟節點而有父節點,則處理父節點
        workInProgress = returnFiber;
        continue;
      } else {
        return null;
      }
    } else {
      ...
  }
  
  return null;
}
複製程式碼

completeWork

根據Fiber的型別進行處理和丟擲錯誤,我們主要看HostComponent型別。對HostComponent型別的處理主要是更新屬性,然後通過createInstance建立DOM節點,並新增進父節點。

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  if (enableProfilerTimer) {
    if (workInProgress.mode & ProfileMode) {
      recordElapsedActualRenderTime(workInProgress);
    }
  }

  switch (workInProgress.tag) {
    ...
    case HostComponent: {
      popHostContext(workInProgress);
      const rootContainerInstance = getRootHostContainer();
      const type = workInProgress.type;
      if (current !== null && workInProgress.stateNode != null) {
        //  更新屬性
        const oldProps = current.memoizedProps;
        const instance: Instance = workInProgress.stateNode;
        const currentHostContext = getHostContext();
        const updatePayload = prepareUpdate(
          instance,
          type,
          oldProps,
          newProps,
          rootContainerInstance,
          currentHostContext,
        );

        updateHostComponent(
          current,
          workInProgress,
          updatePayload,
          type,
          oldProps,
          newProps,
          rootContainerInstance,
          currentHostContext,
        );

        if (current.ref !== workInProgress.ref) {
          markRef(workInProgress);
        }
      } else {
        if (!newProps) {
          ...
          return null;
        }

        const currentHostContext = getHostContext();
        let wasHydrated = popHydrationState(workInProgress);
        if (wasHydrated) {
          if (
            prepareToHydrateHostInstance(
              workInProgress,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            markUpdate(workInProgress);
          }
        } else {
          //  建立並返回DOM元素
          let instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );

          //  將此DOM節點新增進父節點
          appendAllChildren(instance, workInProgress);

          if (
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            markUpdate(workInProgress);
          }
          workInProgress.stateNode = instance;
        }

        if (workInProgress.ref !== null) {
          // If there is a ref on a host node we need to schedule a callback
          markRef(workInProgress);
        }
      }
      return null;
    }
    ...
  }
}
複製程式碼

createInstance

function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): Instance {
  let parentNamespace: string;
  if (__DEV__) {
    ...
  } else {
    parentNamespace = ((hostContext: any): HostContextProd);
  }
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );
  precacheFiberNode(internalInstanceHandle, domElement);
  updateFiberProps(domElement, props);
  return domElement;
}
複製程式碼

createElement

function createElement(
  type: string,
  props: Object,
  rootContainerElement: Element | Document,
  parentNamespace: string,
): Element {
  let isCustomComponentTag;

  const ownerDocument: Document = getOwnerDocumentFromRootContainer(
    rootContainerElement,
  );
  let domElement: Element;
  let namespaceURI = parentNamespace;
  if (namespaceURI === HTML_NAMESPACE) {
    namespaceURI = getIntrinsicNamespace(type);
  }
  if (namespaceURI === HTML_NAMESPACE) {

    if (type === `script`) {
      const div = ownerDocument.createElement(`div`);
      div.innerHTML = `<script><` + `/script>`; // eslint-disable-line
      const firstChild = ((div.firstChild: any): HTMLScriptElement);
      domElement = div.removeChild(firstChild);
    } else if (typeof props.is === `string`) {
      domElement = ownerDocument.createElement(type, {is: props.is});
    } else {
      domElement = ownerDocument.createElement(type);
    }
  } else {
    domElement = ownerDocument.createElementNS(namespaceURI, type);
  }

  return domElement;
}
複製程式碼

總結

到此為止,“React是如何將虛擬DOM轉換為真實DOM”的問題就被解決啦。我們來回顧一下。

  1. 首先我們要通過React.createElement函式來將我們定義好的元件進行轉換為React元素
  2. 將建立好的React元素通過呼叫ReactDom.render來進行渲染
  3. ReactDom.render呼叫後先建立根物件root,然後呼叫root.render
  4. 然後經過若干函式呼叫,來到workLoop函式,它將遍歷虛擬DOM樹,將下一個需要處理的虛擬DOM傳給performUnitOfWork,performUnitOfWork再將虛擬DOM傳給beginWork後,beginWork根據虛擬DOM的型別不同進行相應處理,並對兒子進行處理為Fiber型別,為Fiber型別虛擬DOM新增父節點、兄弟節點等待細節,已方便遍歷樹。
  5. beginWork處理完後返回需要處理的子元素再繼續處理,直到沒有子元素(即返回null),此時performUnitOfWork呼叫completeUnitOfWork處理這顆虛擬DOM子樹,將其轉換為真實DOM。
  6. 最後所有的虛擬DOM都將轉為真實DOM。

除此之外還有很多細節等待我們去研究,比如說React是如何更新和刪除虛擬DOM的?setState的非同步實現原理是怎樣的?以後再寫文和大家分析和探討。

相關文章