前言

Virtual DOM之于React,就好比一个虚拟空间,React的所有工作几乎都是基于Virtual DOM完成的。其中,Virtual DOM模型负责底层框架的构建工作,它拥有一整套的Virtual DOM 标签,并负责虚拟节点及其属性的构建、更新、删除等工作。那么Virtual DOM模型到底是如何构建虚拟节点,如何更新节点属性的呢?

Virtual DOM的概念和诞生背景

首先看DOM的概念,DOM即Document Object Model(文档对象模型)是表征页面元素的一个树形结构。 而Virtual DOM(即虚拟DOM),就是对真实DOM的一个抽象,是用JavaScript来描述的一个对象。Virtual DOM是随着React的诞生而诞生的,由facebook公司提出。它的出现,主要是为了兼顾开发效率与性能。而继React之后,Vue 2.0版本也引入了Virtual DOM的概念。

Virtual DOM的优势

  1. 性能的保障
  2. 提高开发效率
  3. 优秀的跨平台能力

一、为什么我们不直接渲染DOM的更新呢?

真实DOM

React中的Virtual DOM_React从上图我们可以看到一个DOM标签有非常多的属性,DOM操作是非常昂贵的,而看似复杂的Virtual DOM实际上效率更高。

二、DOM标签所需的基本元素

Virtual DOM模型中一个DOM标签所需的基本元素标签有哪些?其实,一套简易Virtual DOM 模型并不复杂,它只需要具备一个DOM标签所需的基本元素即可:

  • 标签名
  • 节点属性(包含样式、属性、事件等)
  • 子节点
  • 标识id

示例代码如下:

{
  // 标签名
  tagName: 'div',
  // 属性
  prototies: {
    // 样式
    style: {}
  },
  // 子节点
  children: [],
  // 唯一标识
  key: 1
}复制代码
{
  type: 'h1',
  props: {
    className: 'title',
    children: 'Hello World!'
  },
  key: null,
  ref: null
}复制代码

Virtual DOM 模型当然不止于此,却也离不开这些基础元素。现在就让我们揭下它的神秘面纱。

Virtual DOM 中的节点称为 ReactNode,它分为3种类型 ReactElement(元素类型)、ReactFragment和ReactText(文本类型)。其中,ReactElement又分为ReactComponentElement(组件类型)和ReactDOMElement(DOM类型)。简单来说就是:元素类型(组件类型、DOM类型)、Fragment类型、文本类型。

下面是ReactNode中不同类型节点所需要的基础元素:

type ReactNode = ReactElement | ReactFragment | ReactText;
type ReactElement = ReactComponentElement | ReactDomElement;
type ReactDOMElement = {
  type: stirng,
  props: {
    children: ReactNodeList,
    className: string,
    etc,
  },
  key: string | boolean | number | null,
  ref: string | null
};
type ReactComponentElement<TProps> = {
  type: ReactClass<TProps>,
  props: TProps,
  key: string | boolean | number | null,
  ref: string | null
};
type ReactFragment = Array<ReactNode | ReactEmpty>;
type ReactNodeList = ReactNode | ReactEmpty;
type ReactText = string | number;
type REactEmpty = nul | undefined | boolean;复制代码

那么,Virtual DOM 模型是如何根据这些节点类型来创建元素的呢?

二、创建React元素

下面是一段JSX与编译后的JavaScript:

const Nav, Profile;
// 输入(JSX);
const app = <Nav color='blue><Profile>click</Profile></Nav>;
// 输出(JavaScript)
const app = React.createElement(
  Nav,
  {color: 'blue'},
  React.createElement(Profile, null, "click")
);复制代码

在线babel编译器

React中的Virtual DOM_Virtual DOM_02

通过 JSX 创建的虚拟元素最终会被编译成调用 React 的 createElement 方法。

// createElement 只是做了简单的参数修正,返回一个 ReactElement 实例对象,
// 也就是虚拟元素的实例
ReactElement.createElement = function(type, config, children) { 
   // 初始化参数
   var propName; 
   var props = {}; 
   var key = null; 
   var ref = null; 
   var self = null; 
   var source = null; 
   
   // 如果存在 config,则提取里面的内容
   if (config != null) { 
     ref = config.ref === undefined ? null : config.ref; 
     key = config.key === undefined ? null : '' + config.key; 
     self = config.__self === undefined ? null : config.__self; 
     source = config.__source === undefined ? null : config.__source; 
    // 复制 config 里的内容到 props(如 id 和 className 等)
     for (propName in config) { 
       if (config.hasOwnProperty(propName) && 
       !RESERVED_PROPS.hasOwnProperty(propName)) { 
       props[propName] = config[propName]; 
       } 
     } 
   } 
   // 处理 children,全部挂载到 props 的 children 属性上。如果只有一个参数,直接赋值给 children,
   // 否则做合并处理
   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]; 
     } 
     props.children = childArray; 
   } 
  // 如果某个 prop 为空且存在默认的 prop,则将默认 prop 赋给当前的 prop
   if (type && type.defaultProps) { 
     var defaultProps = type.defaultProps; 
     for (propName in defaultProps) { 
       if (typeof props[propName] === 'undefined') { 
         props[propName] = defaultProps[propName]; 
       } 
     } 
   }
   // 返回一个 ReactElement 实例对象
   return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props); 
};复制代码

Virtual DOM 模型通过 createElement 创建虚拟元素,那又是如何创建组件的呢?

三、初始化组件入口

当使用React创建组件时,首先会调用instantiateReactComponent,这是初始化组件的入口函数,它通过判断node类型来区分不同组件的入口。

  • 当node为空时,说明node不存在,则初始化空组件ReactEmptyComponent.create(instantiateReactComponent)。
  • 当node类型为对象时,即是DOM 标签组件或自定义组件,那么如果 element 类型为字

符串时,则初始化DOM标签组件 ReactNativeComponent.createInternalComponent (element),否则初始化自定义组件 ReactCompositeComponentWrapper()。

  • 当 node 类型为字符串或数字时,则初始化文本组件 ReactNativeComponent.createInstanceForText(node)。
  • 如果是其他情况,则不作处理。

四、文本组件

当node类型为文本节点时是不算Virtual DOM元素的,但React为了保持渲染的一致性,将其封装为文本组件ReactDOMTextComponent。

在执行mountComponent方法时,ReactDOMTextComponent通过transation.useCreateElement判断该文本是否通过createElement方法创建的节点,如果是,则为该节点创建相应的标签和标 识 domID,这样每个文本节点也能与其他 React 节点一样拥有自己的唯一标识,同时也拥有了Virtual DOM diff 的权利。但如果不是通过 createElement 创建的文本,React 将不再为其创建 <span>和 domID 标识,而是直接返回文本内容。

五、DOM 标签组件

Virtual DOM 模型涵盖了几乎所有的原生 DOM 标签,如 <div>、<p>、<span> 等。当开发者使用 React 时,此时的 <div> 并不是原生 <div> 标签,它其实是 React 生成的 Virtual DOM 对象,只不过标签名称相同罢了。React 的大部分工作都是在 Virtual DOM 中完成的,对于原生 DOM 而言,Virtual DOM 就如同一个隔离的沙盒,因此 React 的处理并不是直接操作和污染原生 DOM,这样不仅保持了性能上的高效和稳定,而且降低了直接操作原生 DOM 而导致错误的风险。

ReactDOMComponent 针对 Virtual DOM 标签的处理主要分为以下两个部分:

  • 属性的更新,包括更新样式、更新属性、处理事件等;
  • 子节点的更新,包括更新内容、更新子节点,此部分涉及 diff 算法(diff算法请看参考文档React列表中的key属性)。
1. 更新属性

当执行 mountComponent 方法时,ReactDOMComponent 首先会生成标记和标签,通过 this.createOpenTagMarkupAndPutListeners(transaction) 来处理 DOM 节点的属性和事件。

  • 如果存在事件,则针对当前的节点添加事件代理,即调用 `enqueuePutListener(this,

propKey, propValue, transaction)`。

  • 如果存在样式,首先会对样式进行合并操作 Object.assign({}, props.style),然后通过

CSSPropertyOperations.createMarkupForStyles(propValue, this) 创建样式。

  • 通过 DOMPropertyOperations.createMarkupForProperty(propKey, propValue) 创建属性。
  • 通过 DOMPropertyOperations.createMarkupForID(this._domID) 创建唯一标识。
2. 更新子节点

当执行 mountComponent 方法时,ReactDOMComponent 会通过this._createContentMarkup(transaction, props, context) 来处理 DOM 节点的内容。

当执行 receiveComponent 方法时,ReactDOMComponent 会通过 this._updateDOMChildren(lastProps, nextProps, transaction, context) 来更新 DOM 内容和子节点。

先是删除不需要的子节点和内容。如果旧节点存在,而新节点不存在,说明当前节点在更新 后被删除,此时执行方法 this.updateChildren(null, transaction, context);如果旧的内容存在,而新的内容不存在,说明当前内容在更新后被删除,此时执行方法this.updateTextContent('')。

再是更新子节点和内容。如果新子节点存在,则更新其子节点,此时执行方法 this.updateChildren(nextChildren, transaction, context);如果新的内容存在,则更新内容,此时执行方法 this.updateTextContent('' + nextContent)。

六、自定义组件

ReactCompositeComponent 自定义组件 实现了一整套 React 生命周期和 setState 机制,因此自定义组件是在生命周期的环境中进行更新属性、内容和子节点的操作。这些更新操作与ReactDOMComponent 的操作类似。


七、总结

React在16版本之后,对Virtual DOM进行了更新,但主要思想还是一致的,想要了解的可参考React Fiber原理解析这篇文章。

Virtual DOM和真实DOM的对比和适用场景

  首次渲染速度表现 更新数据时速度表现 适用场景
Virtual DOM 一般 几乎都适用,在首次渲染和一些特殊场景下会稍慢,但是可以接受。
真实DOM 一般 首次渲染和少量数据操作表现较好,数据量较大时更新操作较慢。

Virtual DOM的性能优势并不完全的体现在“快”,而是体现在它还拥有着更出色的综合性能。