William's Blog

如何理解React的虚拟DOM?

真实的DOM

  在web应用中,浏览器接收到服务器返回的html文档后,会将html文档解析成DOM树,具体流程可以参考这篇博文。DOM树是浏览器在内存中对html文档的抽象化表示,方便开发者使用javascript操作html文档。在传统的前端开发模式下,页面的更新方式大致如图1所示。

传统页面更新方式

图 1 传统的页面更新方式

  传统的web页面更新方式是利用javascript直接操作DOM树,这种做法对于结构简单的页面来说无可厚非,但是对于具有复杂交互功能的页面而言,其效率非常低下。实际上,对于一个大型网站而言,使用javascript对DOM树进行增、删、查、改等操作造成的开销很大,并且无法有效地减少无意义的DOM操作(比如使用javascript动态生成一个列表,就算新列表和旧列表之间只有一列不相同,但是新列表仍然会整个替换旧列表)。除了效率低下之外,传统的前端开发模式编写的代码非常不利于维护,代码中充斥着各种写死的类名和id名以及各种事件绑定函数。

虚拟DOM

  因为频繁地操作真实DOM开销巨大,为了减少无意义的DOM操作,提高页面性能,React在浏览器DOM的基础上进行进一步抽象,实现了一套独立于浏览器的虚拟DOM。所谓虚拟DOM,即一个用来描述真实DOM的javascript对象。React使用ReactElement来对真实的DOM进行抽象,对于每一个真实的DOM元素,如div、span等,React均可以创建一个对应的ReactElement对象。React 16中创建ReactElement对象的createElement方法的源码及笔者标出的注释如下:

// 保留的属性,用户不能修改
const RESERVED_PROPS = {
  key: true,
  ref: true,
  __self: true,
  __source: true,
};

/**
 * 创建ReactElement对象的工厂函数
 * @param {*} type 元素类型
 * @param {*} key 元素的key,React的虚拟DOM diff算法会用到
 * @param {string|object} ref ReactElemet生成的实际DOM的引用
 * @param {*} self 临时变量,用于检测调用React.createElement时'this'与'owner'不同的地方,以便发出警告
 * @param {*} source 一个注释对象,指明文件名、行号或者其他信息
 * @param {*} owner 创建该元素的组件
 * @param {*} props 元素属性
 * @internal
 */
const ReactElement = function(type, key, ref, self, source, owner, props) {
    const element = {
        // $$typeof属性可以用来判断元素类型,REACT_ELEMENT_TYPE是一个常量,表示当前对象为ReactElement类型
        $$typeof: REACT_ELEMENT_TYPE,
        // 类型
        type: type,
        // 标志
        key: key,
        // 生成的真实DOM的引用
        ref: ref,
        // 属性
        props: props,
        // 创建改元素的组件
        _owner: owner,
    };

    // 开发环境下的操作
    if (__DEV__) {
        element._store = {};
        Object.defineProperty(element._store, 'validated', {
            configurable: false,
            enumerable: false,
            writable: true,
            value: false,
        });
        // self属性和source属性只在开发环境下有用
        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;
};

/**
 * @param {*} type 元素类型,取值可以是DOM元素名,如'div', 'span'等;可以取值为ReactComponent类型或 者ReactFragment类型
 * @param {*} config 元素属性
 * @param {*} children 元素子元素,取值可以为多个
 */
function createElement(type, config, children) {
    let propName;
    // Reserved names are extracted
    const props = {};
    let key = null;
    let ref = null;
    let self = null;
    let source = null;
    
    if (config != null) {
        // 判断输入的ref属性和key属性是否合法
        if (hasValidRef(config)) {
          ref = config.ref;
        }
        if (hasValidKey(config)) {
          key = '' + config.key;
        }
        
        // 设置self属性和source属性
        self = config.__self === undefined ? null : config.__self;
        source = config.__source === undefined ? null : config.__source;
        
        // 将config中非保留属性复制到props中
        for (propName in config) {
          if (
            hasOwnProperty.call(config, propName) &&
            !RESERVED_PROPS.hasOwnProperty(propName)
          ) {
            props[propName] = config[propName];
          }
        }
    }
    
    // 可以以多个参数的方式将子元素传入
    const childrenLength = arguments.length - 2;
    if (childrenLength === 1) {
        // 只有一个子元素时,直接赋值给props对象的children属性
        props.children = children;
    } else if (childrenLength > 1) {
        const childArray = Array(childrenLength);
        for (let i = 0; i < childrenLength; i++) {
            childArray[i] = arguments[i + 2];
        }
        // 在开发环境下冻结childArray
        if (__DEV__) {
            if (Object.freeze) {
                Object.freeze(childArray);
            }
        }
        // 拥有多个子元素时,转换成数组赋值给children属性
        props.children = childArray;
    }
    
    // 解析默认属性
    if (type && type.defaultProps) {
        const defaultProps = type.defaultProps;
        for (propName in defaultProps) {
            if (props[propName] === undefined) {
                props[propName] = defaultProps[propName];
            }
        }
    }
    
    // 在开发环境下对key和ref进行处理
    if (__DEV__) {
        if (key || ref) {
            const displayName =
                typeof type === 'function'
                    ? type.displayName || type.name || 'Unknown'
                    : type;
            if (key) {
                // 试图直接访问key属性时出现警告
                defineKeyPropWarningGetter(props, displayName);
            }
            if (ref) {
                // 试图直接访问ref属性时出现警告
                defineRefPropWarningGetter(props, displayName);
            }
        }
    }
    
    // 返回生成的ReactElement对象
    return ReactElement(
        type,
        key,
        ref,
        self,
        source,
        ReactCurrentOwner.current,
        props,
    );
}

  在React中,所有对真实DOM的操作都转换为对虚拟DOM的操作。使用React开发时页面的更新方式可以用图2描述:

使用React框架后页面更新方式

图 2 使用React框架后页面更新方式

  在更新DOM时,React不会直接操作所有ReactElement对象对应的真实DOM,而是首先对比当前新生成的ReactElement对象和旧ReactElement对象之间的差异,然后只操作发生了变化的ReactElement对象对应的真实DOM来更新页面。React减少了无意义的DOM操作,提高了页面性能。
  在React中,ReactElement对象是不可变的,一旦被创建之后,其属性不能被更改,当需要更新页面时,必须重新创建和渲染ReactElement对象。为了更好地组织页面,React引入了ReactComponent的概念。本文重点在于介绍React的虚拟DOM,关于ReactComponent的介绍,笔者会在后续博文中进行总结。

参考文献

  1. React: The Virtual DOM
  2. The difference between Virtual DOM and DOM
  3. reactjs: dom-elements
  4. reactjs: rendering-elements
  5. segmentfault: 图解 React Virtual DOM

William

本博客作者 William 现任职于北京贝壳找房,从事web前端开发相关工作。
您可以通过Email与他取得联系