Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

react中的vdom #57

Open
wangmiaolin opened this issue Nov 13, 2020 · 0 comments
Open

react中的vdom #57

wangmiaolin opened this issue Nov 13, 2020 · 0 comments

Comments

@wangmiaolin
Copy link

什么是虚拟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官网中的一段话:"Virtual DOM 赋予了React 声明式的API:您告诉React希望让UI是什么状态,React 就确保DOM 匹配该状态。这使您可以从属性操作、事件处理和手动 DOM更新这些在构建应用程序时必要的操作中解放出来。"

    ​ 从原生JSJQuery,开发者除了关心数据之外,还需要关心DOM的操作;接着出现了模板引擎,虽然实现了用户界面与业务数据的分离,但是它涉及到大量的字符串的拼接,开发效率也比较低下。为了解决开发体验和开发效率与DOM操作之间的矛盾,虚拟DOM应运而生。

  • 解决跨平台不兼容的问题

    ​ 虚拟DOM是对真实渲染内容的一层抽象,将视图层和渲染平台解耦了,它可以是对Web页面真实DOM的描述,也可以是对IOS界面、安卓界面和小程序等的描述,实现"一套代码,多端运行"

  • 性能问题?

    ​ 操作真实的DOM耗时(JS引擎和渲染引擎互斥,上下文切换耗时;可能导致重排和重绘),但是引入虚拟 DOM 也并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式的同时,仍然保持一个还不错的性能。引用尤大大的一句话:“框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。”

    ​ 性能是一个比较复杂复杂的问题,需要在一个特定的场景下谈论才有意义,举一个极端的例子,如果数据内容整个发生了改变,此时DOM更新的工作量是一致的,而虚拟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 这个位置,对比 D C,发现前后的类型不一致,直接删掉 D 重建 C

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

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

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

有key diff

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant