奇葩说框架之React的渲染流程解析

avatar
公众号:转转技术

文章背景

  本文主要讲两部分内容,一是介绍大致的render函数执行的主流程,包括挂载和渲染jsx节点,对内部调用的函数进行讲解;另一个是细化render阶段做的一些重要内容,包括Fiber的内部计算和任务调度的相关内容。

进入正题

  我们知道,jsx元素要在页面上渲染出来,首先需要调用React.createElement -> React.render(FiberRootNode)-> 创建更新对象 -> 处理更新队列(任务调度) -> 进入commit阶段 -> 渲染完成

  这是render函数执行的一个主流程

  那么,我们先来看看React.createElement函数。

React.createElement

//JSX代码
<div id='1'>1</div>

// 转换成
React.createElement("div", {
	id: "1"
}, "1")

createElement函数将会接收三个参数:type一般为DOM节点名称或类组件、函数组件,config中包含着ref,key,props等配置参数,children就是这个DOM节点内部包含的内容,它可能是前面举例的普通元素,也可能是一个数组。在当前函数中都会做一个判断处理。

function createElement(type, config, children) {
  var propName; 
  var props = {};
  var key = null;
  var ref = null;
  var self = null;
  var source = null;
  if (config != null) {
    if (hasValidRef(config)) {
      ref = config.ref;

      {
        warnIfStringRefCannotBeAutoConverted(config);
      }
    }

    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source; // 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];
      }
    }
  } 
  var childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    var childArray = Array(childrenLength);

    for (var i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }

    {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }

    props.children = childArray;
  } 
  // Resolve default props
  if (type && type.defaultProps) {
    var defaultProps = type.defaultProps;

    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  {if (key || ref) {
      var displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type;
      if (key) {
        defineKeyPropWarningGetter(props, displayName);
      }
      if (ref) {
        defineRefPropWarningGetter(props, displayName);
      }
    }
  }
  return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}

对不同的参数进行处理,然后返回了一个ReactElement对象(一个普通的Object)

var ReactElement = function (type, key, ref, self, source, owner, props) {
  var element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner
  };
  {
    element._store = {}; 
    Object.defineProperty(element._store, 'validated', {
      configurable: false,
      enumerable: false,
      writable: true,
      value: false
    });
    Object.defineProperty(element, '_self', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: self
    });
    Object.defineProperty(element, '_source', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: source
    });
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }
  return element;
};

  其中$$typeof是一个对象类型标识,用于判断当前Object是否一个某种类型的ReactElement props也是一个对象,其中包含了ReactElement上的所有属性,包含children这个特殊属性   创建完ReactElement后,下一步讲调用render的入口函数。

React.render

  我们经常会在react应用的入口页面写上这行代码

ReactDOM.render(<APP />, document.getElementById('root')

  这行代码告诉React应用,我们想在某个容器中渲染出一个组件。那么我们现在就来看看render这个函数到底有什么内容。

  可以看到,它的第一个参数就是前面生成的ReactElement对象。

function render(element, container, callback) {
  return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
}

  render会调用legacyRenderSubtreeIntoContainer进行挂载和更新

function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
  var root = container._reactRootContainer;
  var fiberRoot;
  if (!root) {
    // 通过调用legacyCreateRootFromDOMContainer方法将其返回值赋值给container._reactRootContainer
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      var originalCallback = callback;
      callback = function () {
        var instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    unbatchedUpdates(function () {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      var _originalCallback = callback;
      callback = function () {
        var instance = getPublicRootInstance(fiberRoot);
        _originalCallback.call(instance);
      };
    }
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}

  当我们第一次启动运行项目的时候,也就是第一次执行ReactDOM.render方法的时候,这时去获取container._reactRootContainer肯定是没有值的,所以我们暂时只关心没有root时的代码处理:

  当没有root时,调用legacyCreateRootFromDOMContainer方法将其返回值赋值给container._reactRootContainer,并创建FiberRoot;因为初始化不能批量处理,即同步更新,直接调用unbatchedUpdates方法,这个方法的内容后面再说。

function legacyCreateRootFromDOMContainer(container, forceHydrate) {
  // 判断是否需要融合
  var shouldHydrate = forceHydrate || shouldHydrateDueToLegacyHeuristic(container); 
  // 针对客户端渲染的情况,需要将container容器中的所有元素移除
  if (!shouldHydrate) {
    var warned = false;
    var rootSibling;
    while (rootSibling = container.lastChild) {
      container.removeChild(rootSibling);
    }
  }
  // 返回一个LegacyRoot实例
  return createLegacyRoot(container, shouldHydrate ? {
    hydrate: true
  } : undefined);
}

  这里会调用shouldHydrateDueToLegacyHeuristic函数去判断是否需要融合,当客户端渲染的情况下,需要将容器里的所有元素移除。结尾处调用createLegacyRoot函数去new一个ReactDOMLegacyRoot实例。

function createLegacyRoot(container, options) {
  return new ReactDOMLegacyRoot(container, options);
}

  new ReactDOMLegacyRoot 的内部会执行createRootImpl函数

function ReactDOMLegacyRoot(container, options) {
  this._internalRoot = createRootImpl(container, LegacyRoot, options);
}

  这部分我们要去区分一下FiberRoot和RootFiber的区别和他们之间的联系,往下看

function createRootImpl(container, tag, options) {
  // Tag is either LegacyRoot or Concurrent Root
  var hydrate = options != null && options.hydrate === true;

  var hydrationCallbacks = options != null && options.hydrationOptions || null;

  var mutableSources = options != null && options.hydrationOptions != null && options.hydrationOptions.mutableSources || null;

  var isStrictMode = options != null && options.unstable_strictMode === true;

  // 创建一个fiberRoot
  var root = createContainer(container, tag, hydrate, hydrationCallbacks, isStrictMode);

  // 给container附加一个内部属性用于指向fiberRoot的current属性对应的rootFiber节点
  markContainerAsRoot(root.current, container);

  var rootContainerElement = container.nodeType === COMMENT_NODE ? container.parentNode : container;

  listenToAllSupportedEvents(rootContainerElement);
  
  if (mutableSources) {
    for (var i = 0; i < mutableSources.length; i++) {
      var mutableSource = mutableSources[i];
      registerMutableSourceForHydration(root, mutableSource);
    }
  }
  return root;
}

  从上述源码中,我们可以看到createRootImpl方法通过调用createContainer方法来创建一个fiberRoot实例,并将该实例返回并赋值到ReactSyncRoot构造函数的内部成员_internalRoot属性上。 createFiberRoot方法是创建fiberRoot和rootFiber并相互引用

  我们继续看一下createFiberRoot函数

function createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride) {
  // 通过FiberRootNode构造函数创建一个fiberRoot实例
  var root = new FiberRootNode(containerInfo, tag, hydrate);

  // 通过createHostRootFiber方法创建fiber tree的根节点,即rootFiber
  // 需要留意的是,fiber节点也会像DOM树结构一样形成一个fiber tree单链表树结构
  // 每个DOM节点或者组件都会生成一个与之对应的fiber节点(生成的过程会在后续的文章中进行解读)
  // 在后续的调和(reconciliation)阶段起着至关重要的作用
  var uninitializedFiber = createHostRootFiber(tag, isStrictMode);

  // 创建完rootFiber之后,会将fiberRoot实例的current属性指向刚创建的rootFiber
  root.current = uninitializedFiber;
  // 同时rootFiber的stateNode属性会指向fiberRoot实例,形成相互引用
  uninitializedFiber.stateNode = root;

  {
    var initialCache = new Map();
    root.pooledCache = initialCache;
    var initialState = {
      element: null,
      cache: initialCache
    };
    uninitializedFiber.memoizedState = initialState;
  }
  // 初始化更新队列
  initializeUpdateQueue(uninitializedFiber);
  // 将创建的fiberRoot实例返回
  return root;
}

  通过new一个FiberRootNode构造函数,创建出一个fiberRoot实例,输出实例化结果我们可以看到,FiberRootNode实例包含了很多属性,这些属性在任务调度阶段都发挥着各自的作用

function FiberRootNode(containerInfo, tag, hydrate) {
  this.tag = tag;
  this.containerInfo = containerInfo;
  this.pendingChildren = null;
  this.current = null; //指向当前激活的与之对应的rootFiber节点
  this.pingCache = null;
  this.finishedWork = null;
  this.timeoutHandle = noTimeout;
  this.context = null;
  this.pendingContext = null;
  this.hydrate = hydrate;
  this.callbackNode = null; //每个fiberRoot实例上都只会维护一个任务,该任务保存在callbackNode属性中
  this.callbackPriority = NoLane; // 当前任务的优先级
  //...
}
}

  然后接着createHostRootFiber方法创建fiber tree的根节点,即rootFiber

var createFiber = function (tag, pendingProps, key, mode) {
  // FiberNode构造函数用于创建一个FiberNode实例,即一个fiber节点
  return new FiberNode(tag, pendingProps, key, mode);
};

  FiberNode构造函数用于创建一个FiberNode实例,即一个fiber节点

function FiberNode(tag, pendingProps, key, mode) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null; // Fiber

  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;
  this.ref = null;
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;
  this.mode = mode; // Effects

  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
  this.alternate = null;
}

  至此我们就成功地创建了一个fiber节点,fiber节点会形成一个与DOM树结构对应的fiber tree,并且是基于单链表的树结构,我们刚创建出来的fiber节点作为整个fiber tree的根节点,即RootFiber节点。

  最后回到legacyRenderSubtreeIntoContainer函数,接口最后是执行了

return getPublicRootInstance(fiberRoot)

这行代码

function getPublicRootInstance(container) {
  var containerFiber = container.current;
  if (!containerFiber.child) {
    return null;
  }
  switch (containerFiber.child.tag) {
    case HostComponent:
      return getPublicInstance(containerFiber.child.stateNode);
    default:
      return containerFiber.child.stateNode;
  }
}

  解析一下这个方法做的事情,首先是获取当前fiber节点,即rootFiber;如果rootFiber 还没有子节点则返回 null;其余情况,则去返回containerFiber.child.stateNode子节点的实例

  至此,render函数的主体流程就大体走下来了。


  接下来是第二块内容,我们来细看render阶段所做的事情,对前文没讲到的细分处理做补充。篇幅有限,这里会抽出几个难点函数来讲解...

  首先是回到render函数的入口处

function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
  var root = container._reactRootContainer;
  var fiberRoot;
  if (!root) {
    //...
    unbatchedUpdates(function () {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    //...
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}

  这里有一个updateContainer函数,在是否有root的情况下都调用了。那么这块是做了什么事情?

function updateContainer(element, container, parentComponent, callback) {
  // 根节点Fiber
  var current$1 = container.current;
  var eventTime = requestEventTime();
  var lane = requestUpdateLane(current$1);
  var context = getContextForSubtree(parentComponent);
  if (container.context === null) {
    container.context = context;
  } else {
    container.pendingContext = context;
  }
  // 创建一个更新任务
  var update = createUpdate(eventTime, lane); 
  update.payload = {
    element: element
  };
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    update.callback = callback;
  }
  // 将任务插入Fiber的更新队列
  enqueueUpdate(current$1, update);
  var root = scheduleUpdateOnFiber(current$1, lane, eventTime);
  if (root !== null) {
    entangleTransitions(root, current$1, lane);
  }
  return lane;
}

  这块总结一下,就是先计算出当前更新的一个优先级lane,然后通过createUpdate创建了一个update更新任务,接着通过enqueueUpdate插入循环任务队列,最后调用scheduleUpdateOnFiber函数来调度任务。

  获取优先级这个函数有兴趣可以细看fiber的相关内容,今天咱们看的主要是render的流程,所以直接看scheduleUpdateOnFiber这个函数吧。

scheduleUpdateOnFiber

  scheduleUpdateOnFiber函数在updateContainer函数中被调用,功能是处理优先级和挂载更新节点。

function scheduleUpdateOnFiber(fiber, lane, eventTime) {
  checkForNestedUpdates(); //检查是否有无限更新
  //在root上标记更新,将update的lane放到root.pendingLanes
  markRootUpdated(root, lane, eventTime);
  if (root === workInProgressRoot) {
    {
      workInProgressRootUpdatedLanes = mergeLanes(workInProgressRootUpdatedLanes, lane);
    }
    if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
      markRootSuspended$1(root, workInProgressRootRenderLanes);
    }
  }
  if (lane === SyncLane) {
    if ( // Check if we're inside unbatchedUpdates
    (executionContext & LegacyUnbatchedContext) !== NoContext && // Check if we're not already rendering
    (executionContext & (RenderContext | CommitContext)) === NoContext) {
      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root, eventTime);
      if (executionContext === NoContext && (fiber.mode & ConcurrentMode) === NoMode) {
        resetRenderTimer();
        flushSyncCallbacksOnlyInLegacyMode();
      }
    }
  } else {
    ensureRootIsScheduled(root, eventTime);
  }
  return root;
}

  其中重点在ensureRootIsScheduled这个函数,主要是用于调度任务 ,在多个任务的情况下,相对于新任务,会对现有任务执行、复用、取消的操作;在单个任务的情况下,能对任务进行同步或者异步的调度决策,总之,每次更新和任务退出前都会调用此函数。

function ensureRootIsScheduled(root, currentTime) {
  var existingCallbackNode = root.callbackNode; //获取root.callbackNode,即旧任务
  //记录任务的过期时间,检查是否有过期任务,有则立即将它放到root.expiredLanes
  markStarvedLanesAsExpired(root, currentTime);// 检查任务是否过期,将过期任务放入root.expiredLanes,目的是让过期任务能够以同步优先级去进入调度(立即执行)
  // 获取renderLanes
  var nextLanes = getNextLanes(root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
  if (nextLanes === NoLanes) {
    // 如果渲染优先级为空,则不需要调度
    if (existingCallbackNode !== null) {
      cancelCallback(existingCallbackNode);
    }
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return;
  } 
  var newCallbackPriority = getHighestPriorityLane(nextLanes); // 获取本次任务的优先级
  var existingCallbackPriority = root.callbackPriority; //获取旧任务的优先级
  // 如果新旧任务的优先级相同,则无需调度
  if (existingCallbackPriority === newCallbackPriority) {
    {
      if (existingCallbackNode == null && existingCallbackPriority !== SyncLane) {
        error('Expected scheduled callback to exist. This error is likely caused by a bug in React. Please file an issue.');
      }
    }
    return;
  }
  if (existingCallbackNode != null) {
    // 代码执行到这里说明新任务的优先级高于旧任务的优先级
    // 取消掉旧任务,实现高优先级任务插队
    cancelCallback(existingCallbackNode);
  } 
  var newCallbackNode; //调度一个新任务
  if (newCallbackPriority === SyncLane) {
    // 若新任务的优先级为同步优先级,则同步调度,传统的同步渲染和过期任务会走这里
    if (root.tag === LegacyRoot) {
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }
    {
      // Flush the queue in a microtask.
      scheduleMicrotask(flushSyncCallbacks);
    }
    newCallbackNode = null;
  } else {
    // 根据任务优先级获取Scheduler的调度优先级
    var schedulerPriorityLevel;
    switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority:
        schedulerPriorityLevel = ImmediatePriority;
        break;
      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingPriority;
        break;
      case DefaultEventPriority:
        schedulerPriorityLevel = NormalPriority;
        break;

      case IdleEventPriority:
        schedulerPriorityLevel = IdlePriority;
        break;
      default:
        schedulerPriorityLevel = NormalPriority;
        break;
    }
    // 计算出调度优先级之后,开始让Scheduler调度React的更新任务
    newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
  }
  // 更新root上的任务优先级和任务,以便下次发起调度时候可以获取到
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

  ensureRootIsScheduled函数实际上是在任务调度层面整合了高优先级任务的插队和任务饥饿问题的关键逻辑。想要再深入的话,可以去了解下react的任务更新的处理,收集以及重新调度的相关内容。

结尾

  react的render阶段的流程这块的内容肯定不仅仅只有这篇文章所写的内容,render阶段的工作,细分讲起来可以分为“递”阶段和“归”阶段。其中“递”阶段会执行beginWok,“归”阶段会执行completeWork。本文只是做了一个初始化渲染的流程概述,篇幅有限,感兴趣的同学可以去 react.iamkasong.com/process/rec… 学习下,框架学习,结合源码更香哦。

参考文章


  喜欢就关注我们!