preface

The main purpose of this article is to read the source code process to make notes and share with partners in need, there may be mistakes and mistakes, please readers to judge for themselves, the first time to write reading code article, may be a little messy, any questions welcome to discuss and progress together.

The React version is 16.4. Only some key code is posted on Github.

React Read the series

React

Initialization of the virtual DOM

React.createElement

Before we read the source code, how does React convert the virtual DOM to the real DOM? We read the code more purposefully when we have a question, so let’s think about it with this question in mind.

We often create React element instances using JSX syntax, but they end up being compiled into native JS code with the react.createElement package. Such as:

// class ReactComponent extends React.Component {
// render() {
// return 

Hello React

;
/ /} / /} // The above code will compile to: class ReactComponent extends React.Component { render() { React.createElement( 'p', { className: 'class'}, 'Hello React')}}// React.createElement(ReactComponent, { someProp: 'prop' }, null); Copy the code

This allows us to create React element instances. CreateElement createElement createElement createElement 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)) {
      // If there is a ref, take it out
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      // If there is a key, take it out
      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) ) {// Put special properties like ref, key, etc. in the new props objectprops[propName] = config[propName]; }}}// Get the child element
  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;
  }

  // Add 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,
  );
}
Copy the code
const ReactElement = function(type, key, ref, self, source, owner, props) {
  // The resulting React element
  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;
};
Copy the code

Type is the component type we pass in. It can be a class, function, or string (e.g. ‘div’).

ReactDom.render

We have the React element created, but how do we convert the React element into the DOM we ultimately want? The reactdom. render function is used.

ReactDom.render(
  React.createElement(App),  
  document.getElementById('root'));Copy the code

Definition of reactdom.render:

render(
    element: React$Element<any>,
    container: DOMContainer,
    callback:?Function,) {return legacyRenderSubtreeIntoContainer(
      null./* Parent component */
      element,  /* React element */
      container,  /* DOM container */
      false,
      callback,
    );
  }
Copy the code

LegacyRenderSubtreeIntoContainer take first to React the root container object (only post code) :

. root = container._reactRootContainer = legacyCreateRootFromDOMContainer( container, forceHydrate, ); .Copy the code

Because of too much code only posted by legacyCreateRootFromDOMContainer eventually get the React root container object:

const NoWork = 0;

{
    _internalRoot: {
      current: uninitializedFiber,  // null
      containerInfo: containerInfo,  / / DOM container
      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:?(a)= > mixed
    ) => Work,
    createBatch: (a)= > Batch
}
Copy the code

After initializing the React root container object root, call root.render to start rendering the virtual DOM.

DOMRenderer.unbatchedUpdates((a)= > {
  if(parentComponent ! =null) {
    root.legacy_renderSubtreeIntoContainer(
      parentComponent,
      children,
      callback,
    );
  } else{ root.render(children, callback); }});Copy the code

Because of the large amount of code, not one by one to post detailed code, only post the main function call process.

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)
Copy the code

Fiber

Fiber type:

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, |};Copy the code

beginWork

The beginWork function is used to mount or update components based on the Fiber tag:

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

  // Depending on the component type
  switch (workInProgress.tag) {
    case IndeterminateComponent:
      // Uncertain component type
      return mountIndeterminateComponent(
        current,
        workInProgress,
        renderExpirationTime,
      );
    case FunctionalComponent:
      // Function type component
      return updateFunctionalComponent(current, workInProgress);
    case ClassComponent:
      // A component of the class type
      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.',); }}Copy the code

updateClassComponent

The purpose of updateClassComponent is to initialize uninitialized class components and update and reuse initialized components

function updateClassComponent(current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime,) {
  const hasContext = pushLegacyContextProvider(workInProgress);
  let shouldUpdate;
  if (current === null) {
    if (workInProgress.stateNode === null) {
      // If no instance has been created, initialize it
      constructClassInstance(
        workInProgress,
        workInProgress.pendingProps,
        renderExpirationTime,
      );
      mountClassInstance(workInProgress, renderExpirationTime);

      shouldUpdate = true;
    } else {
      // Reuse the instance if it has already been createdshouldUpdate = resumeMountClassInstance( workInProgress, renderExpirationTime, ); }}else {
    shouldUpdate = updateClassInstance(
      current,
      workInProgress,
      renderExpirationTime,
    );
  }
  return finishClassComponent(
    current,
    workInProgress,
    shouldUpdate,
    hasContext,
    renderExpirationTime,
  );
}
Copy the code

constructClassInstance

Instantiate the class component:

function constructClassInstance(workInProgress: Fiber, props: any, renderExpirationTime: ExpirationTime,) :any {
  const ctor = workInProgress.type;  // The class we passed in
  const unmaskedContext = getUnmaskedContext(workInProgress);
  const needsContext = isContextConsumer(workInProgress);
  const context = needsContext
    ? getMaskedContext(workInProgress, unmaskedContext)
    : emptyContextObject;

  const instance = new ctor(props, context);  // Create an instance
  conststate = (workInProgress.memoizedState = instance.state ! = =null&& instance.state ! = =undefined
      ? instance.state
      : null);
  adoptClassInstance(workInProgress, instance);

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

  return instance;
}
Copy the code

adoptClassInstance

function adoptClassInstance(workInProgress: Fiber, instance: any) :void {
  instance.updater = classComponentUpdater;
  workInProgress.stateNode = instance;  // Assign the instance to the stateNode attribute
}
Copy the code

mountClassInstance

The following code has the familiar componentWillMount life cycle, but newer versions of React do not recommend using it.

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 New life cycle function
    applyDerivedStateFromProps(workInProgress, getDerivedStateFromProps, props);
    instance.state = workInProgress.memoizedState;
  }

  if (
    typeofctor.getDerivedStateFromProps ! = ='function' &&
    typeofinstance.getSnapshotBeforeUpdate ! = ='function' &&
    (typeof instance.UNSAFE_componentWillMount === 'function' ||
      typeof instance.componentWillMount === 'function')) {// If getDerivedStateFromProps is not used, use componentWillMount, compatible with older versions
    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; }}Copy the code

finishClassComponent

Call the render function of the component instance to get the child element to render, and render the child element as Fiber, processing the state and props:

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

  constdidCaptureError = (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 ||typeofctor.getDerivedStateFromCatch ! = ='function')
  ) {
    nextChildren = null;

    if(enableProfilerTimer) { stopBaseRenderTimerIfRunning(); }}else {
    if (__DEV__) {
      ...
    } else {
    // Call the render function to get the child elements
      nextChildren = instance.render();
    }
  }

  workInProgress.effectTag |= PerformedWork;
  if (didCaptureError) {
    reconcileChildrenAtExpirationTime(
      current,
      workInProgress,
      null,
      renderExpirationTime,
    );
    workInProgress.child = null;
  }
  // Convert the child element to Fiber
  // If the number of child elements is greater than one,
  // Return the first subelement of type Fiber
  reconcileChildrenAtExpirationTime(
    current,
    workInProgress,
    nextChildren,
    renderExpirationTime,
  );
  / / processing state
  memoizeState(workInProgress, instance.state);
  / / processing props
  memoizeProps(workInProgress, instance.props);

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

  // Return a child of type Fiber to the beginWork function,
  // all the way back to the workLoop function (see above)
  return workInProgress.child;
}
Copy the code

workLoop

Looking back at the workLoop and performUnitOfWork functions (see the function call above), when benginWork returns child elements after processing the different types of components, WorkLoop continues to call benginWork via performUnitOfWork to process the child elements, thus traversing the virtual DOM tree:

function workLoop(isYieldy) {
  if(! isYieldy) {while(nextUnitOfWork ! = =null) {
      // Traverses the entire virtual DOM treenextUnitOfWork = performUnitOfWork(nextUnitOfWork); }}else {
    while(nextUnitOfWork ! = =null && !shouldYield()) {
      // Traverses the entire virtual DOM tree
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }

    if(enableProfilerTimer) { pauseActualRenderTimerIfRunning(); }}}Copy the code

performUnitOfWork

This function is very close to converting the virtual DOM to the real DOM. When the beginWork returns null, the completeUnitOfWork function starts converting:

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;
}
Copy the code

completeUnitOfWork

This function first converts the current Fiber element into a real DOM node, and then checks whether there are any sibling nodes. If there are any sibling nodes, it returns to the upper-level function for processing and then calls this function for conversion. Otherwise, check whether there is a parent node. If there is, convert the parent node.

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) {
      // Call completeWork to transform the virtual DOM
      let next = completeWork(
        current,
        workInProgress,
        nextRenderExpirationTime,
      );
      stopWorkTimer(workInProgress);
      resetExpirationTime(workInProgress, nextRenderExpirationTime);

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

      // After processing the current node
      if(siblingFiber ! = =null) {
        // If there are sibling nodes, return them
        return siblingFiber;
      } else if(returnFiber ! = =null) {
        // If there is no sibling node but a parent node, the parent node is processed
        workInProgress = returnFiber;
        continue;
      } else {
        return null; }}else{... }return null;
}
Copy the code

completeWork

For handling and throwing errors based on the Fiber type, we’ll focus on the HostComponent type. The HostComponent type is handled mainly by updating the properties and then creating a DOM node with createInstance and adding it to the parent node.

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) {
        // Update attributes
        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) { ... returnnull;
        }

        const currentHostContext = getHostContext();
        let wasHydrated = popHydrationState(workInProgress);
        if (wasHydrated) {
          if( prepareToHydrateHostInstance( workInProgress, rootContainerInstance, currentHostContext, ) ) { markUpdate(workInProgress); }}else {
          // Create and return the DOM element
          let instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );

          // Add this DOM node to the parent node
          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 callbackmarkRef(workInProgress); }}return null; }... }}Copy the code

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;
}
Copy the code

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;
}
Copy the code

conclusion

At this point, the problem of how React converts virtual DOM to real DOM is solved. So let’s go back.

  1. First we’ll use the React. CreateElement function to convert our component into a React element
  2. The React element will be rendered by calling reactdom.render
  3. The reactdom. render call creates the root object root and then root.render
  4. The workLoop function will traverse the virtual DOM tree, pass the next virtual DOM to performUnitOfWork, and then pass the virtual DOM to beginWork. The beginWork is processed according to different types of virtual DOM, and the son is processed as Fiber, and details such as parent node and brother node are added to the virtual DOM of Fiber type, which is convenient to traverse the tree.
  5. The beginWork process returns the child element to be processed and continues until there are no child elements (i.e., null is returned), at which point performUnitOfWork calls completeUnitOfWork to process the virtual DOM subtree and convert it to the real DOM.
  6. Eventually all the virtual DOM will be converted to the real DOM.

How React updates and deletes the virtual DOM? How does asynchronous implementation of setState work? Later write article and everybody analysis and discussion.