kd-cloud-web / Blog

一群人, 关于前端, 做一些有趣的事儿
13 stars 1 forks source link

react中的vdom #57

Open wangmiaolin opened 3 years ago

wangmiaolin commented 3 years ago

什么是虚拟DOM

const ele = (
    <div
        className='myDiv'
        style={{
            width:100,
            height: 100,
            background: 'red'
        }}
    >
    <span>virtual dom</span>
</div>
)

​ 将上面的ele打印出来,如下所示:

虚拟DOm

​ 这就是虚拟DOMReact中的形态,是JSDOM之间的一个映射缓存,它在形态上表现为一个能够描述 DOM 结构及其属性信息的 JS 对象

为什么需要虚拟DOM

React中的虚拟DOM是怎么产生的

​ 虚拟DOM的产生需要借助于Babel

babel编译代码

​ 可以看到JSX标签都被编译成了React.createElement,也就是说JSX本质其实是React.createElement这个JS的语法糖,nameReact.createElement是怎样转化成虚拟DOM的?

createElement的源码如下:

/**
@param {String} type: 同于标识节点的类型,可以是html标签比如div等,也可以是React组件或者React Fragment类型
@param {Object} config: 组件的所有属性,比如className、style等
@param {Object} children: 存储的是组件标签之间嵌套的内容,也就是所谓的子节点、子元素
*/
function createElement(type, config, children) {
    var propName; // 用于存储后面需要用到的元素属性
    var props = {}; // 用于存储元素属性的键值对集合  
    // 定义React元素的属性key、ref、self和source
    var key = null;
    var ref = null;
    var self = null;
    var source = null;

    if (config != null) {
        // 根据入参config依次给ref、key、self和source赋值
        if (hasValidRef(config)) {
            ref = config.ref;
            {
                warnIfStringRefCannotBeAutoConverted(config);
            }
        }
        if (hasValidKey(config)) {
          key = '' + config.key;
        }
        self = config.__self === undefined ? null : config.__self;
        source = config.__source === undefined ? null : config.__source; // Remaining properties are added to a new props object

        // 遍历config中的属性,将符合条件的属性存储到props中
        for (propName in config) {
          if (hasOwnProperty$1.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
            props[propName] = config[propName];
          }
        }
      } // Children can be more than one argument, and those are transferred onto
      // the newly allocated props object.

      // 获取当前元素子节点的个数
      var childrenLength = arguments.length - 2;

      // 单个节点 ==> 直接赋值给props.children
      if (childrenLength === 1) {
        props.children = children;
      } else if (childrenLength > 1) {
        // 多个子节点 ==> 声明一个子节点数组,将子节点放进该数组中,然后赋值给props.children
        var childArray = Array(childrenLength);
        for (var i = 0; i < childrenLength; i++) {
          childArray[i] = arguments[i + 2];
        }
        {
          if (Object.freeze) {
            Object.freeze(childArray);
          }
        }
        props.children = childArray;
      } // Resolve default props

      // 处理defaultProps
      if (type && type.defaultProps) {
        var defaultProps = type.defaultProps;
        for (propName in defaultProps) {
          if (props[propName] === undefined) {
            props[propName] = defaultProps[propName];
          }
        }
      }
      ... // 处理key和ref
      // 返回reactElelemnt方法
      return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}

​ 从上面的源码可以看出来,createElement的主要逻辑就是格式化数据,将开发者传入的参数转换成符合ReactElement方法的参数。ReactElement的源码如下:

const ReactElement = function(type, key, ref, self, source, owner, props) {
    const element = {
        // REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
        $$typeof: REACT_ELEMENT_TYPE,
        // 内置属性赋值
        type: type,
        key: key,
        ref: ref,
        props: props,
        // 父组件
        _owner: owner,
    };
    ...// 针对 __DEV__ 环境下的一些处理逻辑
    }
    return element;
};

​ 从源码可以看出,ReactElelemt的主要逻辑就是把入参分装进element对象,然后将其返回,这个element对象就是一开始我们打印的那个虚拟DOM

​ 现在可以得出的结论是,JSX最终被转化成了一个element对象即虚拟DOM

diff算法

​ 将前后两次的虚拟DOM树进行对比,定位出具体需要更新的部分,生成一个“补丁集”,这就是diff算法。diff算法基于以下三个规律:

​ ①、若两个组件属于同一个类型,那么它们将拥有相同的 DOM 树结构

​ ②、DOM 节点之间的跨层级操作并不多,主要是同层级操作。

​ ③、处于同一层级的一组子节点,可用通过设置 key 作为唯一标识,从而维持各个节点在不同渲染过程中的稳定性

Diff算法的核心要点主要是三个:

​ ①、分层对比

​ 基于“DOM 节点之间的跨层级操作并不多,同层级操作是主流”这一规律,ReactDiff过程直接放弃了跨层级的节点比较,它只针对相同层级的节点作对比。

分层对比

​ 如果存在跨层级的操作,React则直接判断移出子树那一层的组件消失了,对应子树需要被销毁;而移入子树的那一层新增了一个组件,需要重新为其创建一棵子树。销毁 + 重建的代价是昂贵的,因此React官方也建议开发者不要做跨层级的操作,尽量保持 DOM 结构的稳定性。

​ ②、只有类型相同的元素才有Diff的必要

​ 基于“若两个组件属于同一个类型,那么它们将拥有相同的DOM树形结构”这一规律,React认为,只有同类型的组件,才有进一步对比的必要性;若参与Diff的两个组件类型不同,那么直接放弃比较,原地替换掉旧的节点,如下图所示。只有确认组件类型相同后,React才会在保留组件对应 DOM 树(或子树)的基础上,尝试向更深层次去Diff

相同类型才diff

​ ③、属性key可以提高节点的复用性

React官方对key的定义是:key是用来帮助 React 识别哪些内容被更改、添加或者删除。key 需要写在用数组渲染出来的元素内部,并且需要赋予其一个稳定的值。稳定在这里很重要,因为如果key值发生了变更,React则会触发UI 的重渲染。这是一个非常有用的特性。

​ 所以,key想解决的是同一层级中节点的重用问题。如下图所示,如果想在组件A的两个子节点BD之间插入一个新的节点C

无key diff

​ 如果没加key,两棵树之间的 Diff过程应该是这样的:

​ 首先对比位于第 1 层的节点,发现两棵树的节点类型是一致的(都是A),于是进一步Diff

​ 开始对比位于第 2 层的节点,第 1 个接受比较的是B这个位置,对比下来发现两棵树这个位置上的节点都是B,继续下个节点的diff

​ 第 2 个接受比较的是 D这个位置,对比DC,发现前后的类型不一致,直接删掉D 重建C

​ 第 3 个接受比较的是E 这个位置,对比ED,发现前后的类型不一致,直接删掉E重建D

​ 最后接受比较的是树 2 的 E 节点这个位置,这个位置在树 1 里是空的,也就是说树 2 的E是一个新增节点,所以新增一个E

​ 如果有key,如下图所示:

有key diff

​ 那么 key 就可以充当每个节点的 唯一标识,有了这个标识之后,当 C 被插入到BD之间时,React会通过识别 ID,意识到DE 并没有发生变化,只是被调整了顺序而已。接着,React便能够轻松地重用它“追踪”到旧的节点,将 DE 转移到新的位置,并完成对C 的插入。这样一来,同层级下元素的操作成本便大大降低。