从React源码学习React的工作原理之虚拟DOM(二)

284 阅读1分钟

一、虚拟DOM

1、产生的背景

        在传统的Web应用中,数据的变化会实时地更新到用户界面中,一个值的改变可能就会导致页面重排,大大降低页面性能;

        而在实际情况中,DOM节点非常大,操作真实DOM非常耗时,但操作JS原生对象比较简单,所以可以使用JS对象来表示树结构,构建一棵真正的DOM树;【记住:浏览器操作真实DOM比操作原生JS慢 - 浏览器渲染DOM节点慢】

        浏览器渲染线程与JS引擎线程是互斥的,在浏览器渲染页面时,不可交互;

2、虚拟DOM

(1)定义
        虚拟DOM就是一个用来表示真实DOM的普通JS对象,包含type、key、ref、props、children等属性;【虚拟DOM的本质就是在JS对象和真实DOM之间做了一层缓存】
        虚拟DOM与真实DOM节点一一对应;更改虚拟DOM不会引起浏览器的重排和重绘;
        【虚拟DOM的目的】将所有的操作聚集到一块,计算出所有的变化后,统一更新一次虚拟DOM;

(2)虚拟DOM结构

// 虚拟DOM的结构 
const vNode = { 
    $$typeof: Symbol(react.element), 
    key: null, 
    props: { 
        children: [ 
            { 
                type: 'span', 
                ... 
            }, 
            ... 
        ], 
        className: 'active', 
        onClick: () => {} 
    }, 
    ref: null, // 指向真实DOM节点 
    type: 'div', // 元素类型 
    _owner: FiberNode, 
    _store: {
        validated: false
    }, 
    _self: undefined 
}

(3)虚拟DOM的优劣

【虚拟DOM的优点】
  • 性能提升 - 减少对真实DOM的操作;【减少的是DOM操作的次数】
    • 可以将多次操作合并为一次操作;
    • 虚拟DOM借助DOM diff可以把多余的操作省掉;
  • 跨平台:虚拟DOM本质上是一个JS对象;
    • 虚拟DOM抽象了原本的渲染过程,实现了跨平台的能力;
  • 抹平了浏览器的兼容性问题,避免用户操作真实DOM;
  • 内容经过XSS处理,可以有效防范XSS攻击;
  • 提升效率:不用关心节点操作,而是关注业务逻辑;
【虚拟DOM的缺点】
  • 消耗内存:需要额外创建函数;
  • 需要依赖Babel转义JSX;
  • 首次渲染大量DOM时,由于多了一层虚拟DOM的计算,速度比innerHTML插入慢;

3、虚拟DOM与原生DOM的区别

(1)结构区别

  • 真实DOM:文档对象模型,在页面渲染出的每一个节点都是一个真实的DOM节点;
    • 真实DOM节点会包含很多属性和方法,有些属性和方法在实际中不会使用到;
  • 虚拟DOM:返回的是一个JS对象;
    • 虚拟DOM节点包含了DOM节点的属性、文本、事件对象等信息;没有多余的属性和方法;
  • 真实DOM包含很多属性,虚拟DOM比真实DOM轻;

(2)操作区别

  • 浏览器操作 JS 很快,也就是操作虚拟DOM很快;
  • 操作虚拟DOM不会进行排版与重绘操作,而频繁操作真实DOM节点会触发浏览器的重排和重绘;

(3)渲染区别

  • 真实DOM:每次数据的变化都会引起DOM的渲染;
    • 如果某次操作需要更新10个DOM节点,浏览器在收到第一个更新DOM请求后,并不知道后续还有9个更新操作,因此会马上执行流程,最终会执行10次;
    • 使用原生JS操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程;
  • 虚拟DOM:将所有操作聚集到一块,计算出变化后,统一更新DOM;
    • 使用虚拟DOM更新10个DOM节点时,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个JS对象中,最终将这个JS对象一次性attach到DOM树上,避免大量无谓的计算。

4、虚拟DOM性能提升

        虚拟DOM提升性能的点在于:DOM发生变化的时候,通过diff算法计算出需要变更的DOM,只对变化的DOM进行操作,而不是更新整个视图;
        也就是说,一个页面如果有100次变化,假设没有虚拟DOM,页面就会渲染500次;而虚拟DOM只需要渲染一次,可以减少DOM操作次数;

【虚拟DOM一定会提升性能吗】

不一定;

虚拟DOM只是减少DOM操作,但是不能避免DOM操作;它的优势在于diff算法和批量处理策略,收集DOM操作统一处理;

React的虚拟DOM只能保证在更新时比较快,但是首次渲染可能会比原生DOM慢;

当DOM规模较小时,React的性能比原生JS好;但是规模较大时,React可能会崩;

二、虚拟DOM的实现

1、如何构建虚拟DOM?

        React.createElement用来创建虚拟DOM,返回的就是一个虚拟DOM对象;React再将虚拟DOM对象转换为真实的DOM节点显示到页面中。
        最新版本中,通过JSX方法创建虚拟DOM;

// createElement创建虚拟DOM 
const element = React.createElement( 
    'div', 
    { 
        className: 'element' 
    }, 
    'Hello, world!' 
); 
// JSX创建虚拟DOM:可以通过Babel将下述代码转为createElement 
const element = <div className='element'>Hello, world!</div>;

2、如何将虚拟DOM转换为真实DOM?

(1)转换过程

        JSX运行时,Babel会将JSX转为React.createElement执行后的对象,即虚拟DOM对象;再通过ReactDOM.render函数将虚拟DOM转为真实DOM;

(2)render方法的参数

render方法接收三个参数:
        第一个参数表示渲染的节点,即JSX|虚拟DOM;
        第二个参数表示容器;
        第三个参数是一个回调函数,是可选的;

三、虚拟DOM源码解析

        需要注意的是:虚拟DOM对象和React Fiber是两个完全不同的东西.

以下代码是React18版本
// 保留属性: 
const RESERVED_PROPS = { 
    key: true, // 唯一标识 
    ref: true, // ref:指向元素本身的属性 
}; 
export function jsx(type, config, maybeKey) { 
    let propName; 
    const props = {}; 
    let key = null; 
    let ref = null; 
    if (maybeKey !== undefined) { 
        key = '' + maybeKey; 
    } 
    if (hasValidKey(config)) { 
        key = '' + config.key; 
    } 
    if (hasValidRef(config)) { 
        ref = config.ref; 
    } 
    // 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]; 
        } 
    } 
    // Resolve default props 
    if (type && type.defaultProps) { 
        const defaultProps = type.defaultProps; 
        for (propName in defaultProps) { 
            if (props[propName] === undefined) { 
                props[propName] = defaultProps[propName]; 
            } 
        } 
    } 
    return ReactElement( 
        type, 
        key, 
        ref, 
        undefined, 
        undefined, 
        ReactCurrentOwner.current, 
        props, 
    ); 
}

const ReactElement = function(type, key, ref, self, source, owner, props) { 
    const element = { 
        // This tag allows us to uniquely identify this as a React Element 
        // 用于表示组件的类型:使用十六进制或者Symbol值表示; 
        // React在最终渲染DOM的时候,需要确保元素的类型是React-Element-type;需要使用该属性作为判断依据 
        $$typeof: REACT_ELEMENT_TYPE, 
        type: type, // 用于表示元素的具体类型:div span 
        key: key, // 用于diff算法 
        ref: ref, 
        props: props, // 属于元素自己的属性 
        // Record the component responsible for creating this element. 
        _owner: owner, 
    }; 
    return element; 
};