React v16源码之ReactDOM.render

前言

React提供render API来实现将组件构成的视图渲染挂载到指定的DOM节点上,本文旨在梳理ReactDOM.render函数的执行逻辑,从而进一步理解React 16版本之后的Fiber以及相关处理过程(React版本是16.14.0)。

render API

render API是属于react-dom包的,工作就是将组件渲染成DOM并挂载到DOM中,该API整体执行逻辑如下:

react-render

从上图可知render API的整体主要的处理逻辑,具体点的处理逻辑这里展开来梳理:

isValidContainer

该函数判断挂载点是否合法,实际上就是判断挂载点是否是元素、document、documentFragement、特定的注释节点。

实际上在判断完挂载点的合法性之后,存在isContainerMarkedAsRoot函数调用,该函数判断DOM节点是否存在__reactContainer$开头的相关属性

legacyRenderSubtreeIntoContainer

该函数的逻辑实际上区分是初始化挂载阶段还是更新阶段,不同的阶段有不同的逻辑处理。这里暂时不看更新阶段具体的处理,其中对于初始化阶段(root为undefined)的处理,从前面梳理的逻辑图中可知是主要调用下面2个函数来做进一步的处理:

  • legacyCreateRootFromDOMContainer
  • unbatchedUpdates
legacyCreateRootFromDOMContainer

该函数实际上就是创建并返回root对象,而其直接的逻辑处理很简单:

  • 处理挂载点下存在data-reactroot标签的元素的一些处理

  • 调用createLegacyRoot:该函数的作用就是实例化ReactDOMBlockingRoot,即new ReactDOMBlockingRoot操作

    function createLegacyRoot(container, options) {
    	return new ReactDOMBlockingRoot(container, 0, options);
    }
    
    function ReactDOMBlockingRoot(container, tag, options) {
    	this._internalRoot = createRootImpl(container, tag, options);
    }
    

ReactDOMBlockingRoot函数实际上是作为构造函数调用的,有render、unmount这两个实例方法,其内部定义_internalRoot属性,其属性值是调用createRootImpl生成的。

createRootImpl

createRootImpl的逻辑主要如下(剔除了与服务器渲染相关的逻辑):

function createRootImpl() {
	...
	var root = createContainer(container, tag, hydrate);
    markContainerAsRoot(root.current, container);
    var rootContainerElement = container.nodeType === COMMENT_NODE ? container.parentNode : container;
    listenToAllSupportedEvents(rootContainerElement);
    ...
    return root;
}

主要逻辑点如下:

  • 调用createContainer创建root对象
  • markContainerAsRoot
  • listenToAllSupportedEvents

createContainer创建root对象

通过createContainer创建root对象具体是什么?实际上背后是调用createFiberRoot函数来实现的,其源码逻辑如下:

function createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks) {
	var root = new FiberRootNode(containerInfo, tag, hydrate);
	// stateNode is any.
	var uninitializedFiber = createHostRootFiber(tag);
	root.current = uninitializedFiber;
	uninitializedFiber.stateNode = root;
	initializeUpdateQueue(uninitializedFiber);
   	return root;
}

FiberRootNode是一个构造函数,它的实例就是root对象即root对象是一个Node作为fiber root,这涉及到Fiber架构中的概念。

Fiber 是 React 16 中新的协调引擎,它的主要目的是使 Virtual DOM 可以进行增量式渲染。

createHostRootFiber函数的逻辑如下:

// React支持不同的模式(这是Fiber架构带来),例如Concurrent Mode、Blocking Mode等,具体模式的区别这里不细究知道就行
createFiber(3, null, null, mode);

var createFiber = function (tag, pendingProps, key, mode) {
	return new FiberNode(tag, pendingProps, key, mode);
};

实例化FiberNode, Fiber本质上就是一个JavaScript对象,它存储着相关的信息。在FiberNode中有三个属性:return(指向父节点)、child(子节点)、slibling(兄弟节点),这三个属性值值可以是null或FiberNode对象,通过这三个属性就会形成类似树形结构。

之后会执行initializeUpdateQueue的逻辑,实际上就是在fiberNode对象上定义updateQueue属性。

从上面的整个处理过程可知:

在初始化挂载阶段会生成root对象(实际上就是实例化FiberRoot Node),并且会创建一个Fiber Node对象与root对象进行关联, 即root.current = FiberNode

markContainerAsRoot

该函数的就是在挂载点DOM上定义__reactContainer$开头的属性,而属性值就是当前FiberRoot的current值,即FiberNode。

listenToAllSupportedEvents

React中是合成事件机制,所有的事件都是事件委托的形式来处理的,还函数就是注册所有被支持的事件,其处理逻辑如下:

  var listeningMarker = '_reactListening' + Math.random().toString(36).slice(2);
  function listenToAllSupportedEvents(rootContainerElement) {
    if (!rootContainerElement[listeningMarker]) {
      // 添加标记避免多次绑定事件
      rootContainerElement[listeningMarker] = true;
      // 所有支持的事件都通过listenToNativeEvent来实现注册,其中对于selectionchange需要特殊处理
      allNativeEvents.forEach(function (domEventName) {
        if (domEventName !== 'selectionchange') {
          if (!nonDelegatedEvents.has(domEventName)) {
            listenToNativeEvent(domEventName, false, rootContainerElement);
          }

          listenToNativeEvent(domEventName, true, rootContainerElement);
        }
      });
      var ownerDocument =
      	rootContainerElement.nodeType === DOCUMENT_NODE
      		? rootContainerElement
      		: rootContainerElement.ownerDocument;
	  // selectionchange需要注册到Document上
      if (ownerDocument !== null) {
        if (!ownerDocument[listeningMarker]) {
          ownerDocument[listeningMarker] = true;
          listenToNativeEvent('selectionchange', false, ownerDocument);
        }
      }
    }
  }

事件注册的具体逻辑这里暂不细究,只需要知道是注册所有被支持的事件到挂载点DOM(其中selectionchange注册到Document上)即可。

unbatchedUpdates

该函数会涉及到React中一个批量更新的机制,而在初始化挂载阶段该函数调用如下:

function unbatchedUpdates(fn, a) {
	...
    try {
      	return fn(a);
    } finally {
    	...
    }
}

unbatchedUpdates(function () {
	updateContainer(element, fiberRoot, parentComponent, callback);
});

通过源码unbatchedUpdates内部实际上就是执行传入的函数,此处即调用updateContainer:

function updateContainer(element, container, parentComponent, callback) {
	...
    var current$1 = container.current;
    ...
    var update = createUpdate(eventTime, lane);
    update.payload = {
      element: element
    };
    enqueueUpdate(current$1, update);
    scheduleWork(current$1, expirationTime);
}

上面是简化后的逻辑,实际上updateContainer中创建一个update对象,该update对象中包含当前处理时的高精度时间等,其中payload数据中保存这着根组件对应的React元素对象,由于后续的渲染等处理。

enqueueUpdate

该函数是根据相关条件排列update,实际上的处理逻辑归纳如下:

function enqueueUpdate(fiber, update) {
	var updateQueue = fiber.updateQueue;

    if (updateQueue === null) {
      // Only occurs if the fiber has been unmounted.
      return;
    }

    var sharedQueue = updateQueue.shared;
    var pending = sharedQueue.pending;
    // 处理pending与当前update之间的顺序问题
    ...
    
    // updateQueue.shared.pending
    sharedQueue.pending = update;
  }

如果当前的fiber对象存在updateQueue,就会设置其对应的pending属性值为传入的update对象。

scheduleWork

该函数实际上是React Fiber处理的核心逻辑,非常复杂,这里也简单聊聊针对初始化挂载阶段的主要处理逻辑有如下几点:

  • 会依据current Fiber对象创建一个workInProgress Fiber对象
  • workLoopSync函数调用
  • commitRoot函数调用:在这个阶段就会创建真实DOM并挂载到指定节点

workLoopSync的执行逻辑如下:

function workLoopSync() {
    while (workInProgress !== null) {
      performUnitOfWork(workInProgress);
    }
}

实际上是遍历处理workInProgress Fiber对象,对workInProgress Fiber对象调用performUnitOfWork函数来处理,而performUnitOfWork函数中有2点逻辑非常重要:

function performUnitOfWork(unitOfWork) {
    var next;
    ...
    next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
    ...

    if (next === null) {
      completeUnitOfWork(unitOfWork);
    } else {
      workInProgress = next;
    }
    ...
}

执行beginWork$1函数

背后调用beginWork函数,该函数的主要逻辑就是根据Fiber对象的tag调用对应的更新函数(核心参数是current Fiber 与 workInProgress Fiber)。涉及到workInProgress类型有:

switch (workInProgress.tag) {
	// 不确定的组件,可能是函数组件、class组件
	case IndeterminateComponent:
    // lazy组件	
    case LazyComponent:
    // 函数组件
    case FunctionComponent:
    // class组件
    case ClassComponent:
    // host tree root
    case HostRoot:
    case SuspenseComponent:
    case Fragment:
    case Profiler:
    case ContextProvider:
    case ContextConsumer:
    case MemoComponent:
    ...
}

从上面实际上对应着相关的组件类型,实际上每一个组件都会对应一个FiberNode对象。这里就涉及到相关组件的实例化,例如:

class组件在源码中会调用updateClassComponent函数,而该函数中就会涉及到:constructClassInstance、mountClassInstance、finishClassComponent等逻辑,即new实例化、组件实例对象instance相关处理、完成class组件的处理

constructClassInstance函数中会触发class组件的构造函数的执行,finishClassComponent函数中会调用class组件的render函数。

workInProgress重新赋值

当beginWork$1执行完后就会返回一个值赋值给next,当next存在时就对workInProgress重新赋值,这样就会继续遍历,直到next不存在,通过设置next这个过程就可以处理到每一个组件。

commitRoot
当处理完所有组件的渲染之后,就会执行commitRoot的逻辑,而该函数的逻辑主要就是执行下面的函数:

runWithPriority$1(ImmediatePriority, commitRootImpl.bind(null, root, renderPriorityLevel));

第一个参数是优先级,第二个参数就是需要执行的回调函数,这里是commitRootImpl函数,在commitRootImpl函数中主要的逻辑是执行三个函数:

  • commitBeforeMutationEffects
  • commitMutationEffects
  • commitLayoutEffects

其中commitMutationEffects函数中就会将React元素对应的DOM插入到页面中,当整个阶段结束后页面上就已经显示出了组件的UI内容,但是在源码中相关逻辑还未结束,之后会执行flushSyncCallbackQueueImpl等相关函数的逻辑,至此完成整个挂载处理流程。

更新阶段render的处理

实际上上面的整个流程的梳理都是基于挂载阶段的处理,那么更新阶段相关处理会有什么不同吗?实际上整个流程最明显的不同点处理是legacyRenderSubtreeIntoContainer函数中,实际上在前面也有提及该函数中区分挂载阶段和更新阶段的逻辑,只是没有细究,主要的相关处理逻辑如下:

function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
     if (!root) {
       // Initial mount
       ...
       // Initial mount should not be batched.
       unbatchedUpdates(function () {
         updateContainer(children, fiberRoot, parentComponent, callback);
       });
     } else {
       ...
       updateContainer(children, fiberRoot, parentComponent, callback);
     }
     ...
}

挂载阶段不应该批量处理,所以调用了unbatchedUpdates函数,在前面的逻辑梳理中,实际上已经知道该函数内部还是执行传递的函数参数,即主要执行updateContainer,实际上还有其他逻辑的执行,例如:

function unbatchedUpdates(fn, a) {
	// 上下文设置
     try {
       return fn(a);
     } finally {
       executionContext = prevExecutionContext;
       // 同步处理callback
       if (executionContext === NoContext) {
         // Flush the immediate callbacks that were scheduled during this batch
         flushSyncCallbackQueue();
       }
     }
 }

而更新阶段就是直接执行updateContainer函数,而updateContainer函数可以说是非常核心的功能逻辑,主要逻辑有两点:

  • 生成update对象(createUpdate)并将其加入到队列中(enqueueUpdate)
  • 调度开始工作(scheduleWork)

需要注意的是legacyRenderSubtreeIntoContainer函数的触发,并不是所有更新都是通过该函数的,该函数的调用情况还值得推敲。

总结

对render函数的处理过程简单的梳理了下,其中涉及到Fiber的处理过程非常复杂,但是有几点明确的信息:

  • 在Fiber处理过程中,会存在current状态与workInProgress状态的Fiber对象存在
  • 每一个组件都会生成一个React对象,而每一个React对象也会对应一个FiberNode对象
  • 在React中存在React元素、FiberNode这两个概念,实际上Fiber架构处理过程中是针对FiberNode,其通过相关属性组成树状结构,而React元素主要是提供组件内容的载体以及信息
  • React元素与FiberNode对象之间建立联系是通过updateQueue,该属性中对应着一个update对象,该对象载荷数据就是React元素即要渲染的组件对象

render这个过程中实际上最核心的就是Fiber处理,不得不说这个过程是真的非常复杂,其中涉及到的细节处理太多,理解起来还是非常困难的,特别是涉及到优先级的调度。

Render函数的整体处理流程如下:

render处理整体流程

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值