IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> React v16源码之ReactDOM.render -> 正文阅读

[JavaScript知识库]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);
    scheduleUpdateOnFiber(current$1, lane, eventTime);
    return lane;
}

上面是简化后的逻辑,实际上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对象。

scheduleUpdateOnFiber

该函数实际上是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这个过程就可以处理到每一个组件。

总结

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

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

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

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

render处理整体流程

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-09-02 11:15:56  更:2021-09-02 11:17:59 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/27 20:26:26-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计