react虚拟Dom和Diff算法

196 阅读5分钟

为什么会有虚拟Dom?

一个DOM元素创建后,就拥有了HTML Element、Element两种属性,前者是继承于父类和全局的属性和方法,后者是相同种类的元素所普遍具有的方法和属性,我们都是知道随便一个标签上的属性都是一大串。一个网页的DOM很多,同时又涉及到层层嵌套,仅仅依靠浏览器来一次又一次的渲染html效率是非常低的。

但是我们发现,js依靠轻量的特性和浏览器强大的引擎执行起来是非常快的。用js直接操作真实DOM显然不现实的,因为js操作DOM很麻烦,代码可读性差,不但DOM生成费时间,js运行也费时间。

操作真实DOM不行,那么操作一个用js模拟的“假DOM”,得到结果了再生成真DOM不就快很多了吗?于是虚拟DOM的模式就出现了。

react给虚拟Dom的定义?能做什么?

Virtual DOM 是一种编程概念。在这个概念里, UI 以一种理想化的,或者说“虚拟的”表现形式被保存于内存中,并通过如 ReactDOM 等类库使之与“真实的” DOM 同步。

通过js的形式记录DOM该有的UI状态,然后又避免了大多数通过js来操作属性、事件处理、手动操作DOM场景(直接用react提供的形式或html语言,如jsx语法、组件、类或者直接写标签)

react的虚拟DOM怎么做的呢?

jsx语法通过babel编译后每个标签都会执行createElement函数 我们也可以通过一下方式直接调用

React.createElement(component, props, ...children)

createElement又做了什么?直接上源码

const RESERVED_PROPS = {
  key: true,
  ref: true,
  __self: true,
  __source: true,
};

export 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;

  //将config上有但是RESERVED_PROPS上没有的属性,添加到props上
  //将config上合法的ref与key保存到内部变量ref和key
  if (config != null) {
    //判断config是否具有合法的ref与key,有就保存到内部变量ref和key中
    if (hasValidRef(config)) {
      ref = config.ref;
                {
          warnIfStringRefCannotBeAutoConverted(config);
        }
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    //保存self和source
    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的propName属性上
    for (propName in config) {
      if (
        hasOwnProperty.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.
  //  如果只有三个参数,将第三个参数直接覆盖到props.children上
  //  如果不止三个参数,将后面的参数组成一个数组,覆盖到props.children上
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    if (__DEV__) {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;
  }

  // Resolve default props
  //  如果有默认的props值,那么将props上为undefined的属性设置初始值
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  //开发环境
  if (__DEV__) {
      //  需要利用defineKeyPropWarningGetter与defineRefPropWarningGetter标记新组件上的props也就		是这里的props上的ref与key在获取其值得时候是不合法的。
    if (key || ref) {
      //type如果是个函数说明不是原生的dom标签,可能是一个组件,那么可以取
      const displayName =
        typeof type === 'function'
          ? type.displayName || type.name || 'Unknown'
          : type;
      if (key) {
        //在开发环境下标记获取新组件的props.key是不合法的,获取不到值
        defineKeyPropWarningGetter(props, displayName);
      }
      if (ref) {
        //在开发环境下标记获取新组件的props.ref是不合法的,获取不到值
        defineRefPropWarningGetter(props, displayName);
      }
    }
  }
  //注意生产环境下的ref和key还是被赋值到组件上
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

总结:创建key,ref,props,self等重要参数,同时将多于两个的children处理成数组,并调用ReactElement方法

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // This tag allows us to uniquely identify this as a React Element 是否标识为唯一的reactElement
    $$typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    type: type,//分为原生标签,class标签,function标签,特殊的标签,其他
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element. //创建该元素的组件
    _owner: owner,
  };
	//开发环境 会创建三个对象_store,_self,_source
  if (__DEV__) {
    element._store = {};

    // To make comparing ReactElements easier for testing purposes, we make
    // the validation flag non-enumerable (where possible, which should
    // include every environment we run tests in), so the test framework
    // ignores it.
    // 验证属性
    Object.defineProperty(element._store, 'validated', {
      configurable: false,
      enumerable: false,
      writable: true,
      value: false,
    });
    // self and source are DEV only properties.
    Object.defineProperty(element, '_self', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: self,
    });
    // Two elements created in two different places should be considered
    // equal for testing purposes and therefore we hide it from enumeration.
    // 应该考虑在两个不同位置创建的两个元素
    Object.defineProperty(element, '_source', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: source,
    });
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }

  return element;
};

总结:新增了$$typeof、_owner属性,返回一个对象,并记录下来

_owner是记录创建它的组件,$$typeof是干嘛的呢?通过翻译:是否是reactElement唯一标识,感觉没啥用。但是打印出来看看

可以发现$$typeof是个Symbol类型的对象,众所周知:Symbol是一个不可改变的匿名类型,通常在类中作为标识符,也不能被传统的方法遍历出来,是全局唯一的数据类型。

/**
 * Verifies the object is a ReactElement. 判断这个对象是否是合法ReactElement
 * See https://reactjs.org/docs/react-api.html#isvalidelement
 * @param {?object} object
 * @return {boolean} True if `object` is a ReactElement.
 * @final
 */

function isValidElement(object) {
  return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
}

那么$$typeof用处是什么呢?

安全
数据库是无法存储 Symbol 类型数据的,所以用户恶意存入的数据是无法带有合法的 typeof字段的。当React在渲染的时候加上对typeof 字段的。 当 React 在渲染的时候加上对 typeof 合法性的验证即可防止恶意代码的插入。有效防止了XSS攻击,低版本不支持 Symbol 的浏览器是没有这个安全特性的

通过Babel编译后及两个函数处理后,dom在js中以对象的形式存储起来,这就是react中的虚拟DOM 有个问题,每次更新都去调用这些方法是不是太浪费了?如何有效的更新UI又保证不重复执行这些方法?

答案就是利用算法来计算,将新旧“DOM树” 进行对比,然后差异性在更新,但是现有算法复杂度太高

Diff算法诞生了,有两个假设:

  1. 两个不同类型的元素会产生出不同的树;

  2. 开发者可以通过 key 来暗示哪些子元素在不同的渲染下能保持稳定;

Diff算法做什么?

1、首先对比根元素,只要不同直接重新生成新的树,及销毁组件及子组件

2、对比同类元素(组件),保留整个DOM,只更新上面的属性,并进行递归

新的问题

diff算法是同级比较再递归,末尾插入新DOM,删除修改DOM都只是操作一次就行

<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

但是再开头就插入新DOM,导致所有DOM位置改变,意味着所有DOM全部都要操作一遍

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

key出现了

作为唯一的key,diff函数只需要对比前后相同key的同级元素,不同的更新就行了,这样算法的复杂度就变为了O(n)(n为元素个数)

我们发现平时我们敲jsx的时候,只有map循环的时候才写了key,不写就警告啥的,其他情况没写也没什么问题;还有就是之前没有警告key,莫名其妙突然就警告key相同,怎么回事?

问题一:在render阶段会检查dom是否有key 没有就会创建一个作为key然后进行初次渲染;但是在map时,由于数据是异步的,所以警告是数据回来后做diff时候报的,并非初次渲染,初始渲染应该是[]元素渲染的时候,就是“白屏”时候

//子为数组时候的部分diff算法   处理无key值  
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, expirationTime) {
    // This algorithm can't optimize by searching from both ends since we
    // don't have backpointers on fibers. I'm trying to see how far we can get
    // with that model. If it ends up not being worth the tradeoffs, we can
    // add it later.
    // Even with a two ended optimization, we'd want to optimize for the case
    // where there are few changes and brute force the comparison instead of
    // going for the Map. It'd like to explore hitting that path first in
    // forward-only mode and only go for the Map once we notice that we need
    // lots of look ahead. This doesn't handle reversal as well as two ended
    // search but that's unusual. Besides, for the two ended optimization to
    // work on Iterables, we'd need to copy the whole set.
    // In this first iteration, we'll just live with hitting the bad case
    // (adding everything to a Map) in for every insert/move.
    // If you change this code, also update reconcileChildrenIterator() which
    // uses the same algorithm.
    {
      // First, validate keys.
      var knownKeys = null;

      for (var i = 0; i < newChildren.length; i++) {
        var child = newChildren[i]; //循环所有的子元素
        knownKeys = warnOnInvalidKey(child, knownKeys); //执行warnOnInvalidKey函数
      }
    }
}

/**
   * Warns if there is a duplicate or missing key
   */

  function warnOnInvalidKey(child, knownKeys) {
    {
      if (typeof child !== 'object' || child === null) { //判断child是否为空或对象(虚拟DOM为对象)
        return knownKeys;
      }
    //和前面我们猜想一样 首先会利用判断是否是reactElement
      switch (child.$$typeof) {
        case REACT_ELEMENT_TYPE:
        case REACT_PORTAL_TYPE:
          warnForMissingKey(child); //如果ReactDOM.createPortal创建的portal类型会警告他缺少key
          var key = child.key;

          if (typeof key !== 'string') {
            break; //key不是字符串也是合法的
          }

          if (knownKeys === null) {
            knownKeys = new Set(); //如果key是空的  会new一个Set类型 Set内的属性是不可重复的
            knownKeys.add(key);//并在该类型上加上key
            break;
          }

          if (!knownKeys.has(key)) {
            knownKeys.add(key); //Set里没有该key 也就是判断是否key相同会添加上 
            break;
          }

          error('Encountered two children with the same key, `%s`. ' + 'Keys should be unique so that components maintain their identity ' + 'across updates. Non-unique keys may cause children to be ' + 'duplicated and/or omitted — the behavior is unsupported and ' + 'could change in a future version.', key);
            //不为null,且Set中有该key  则表示存在相同的key   返回一个错误
          break;
      }
    }

    return knownKeys;
  }

问题二:在执行更新操作,非初次渲染时才会进行diff对比,此时同级的相同兄弟元素没有key都是null判定为相同便发出警告

几点建议:同级相同元素必须带上key,不做curd操作可以使用index作为key,尽量在key前面带上私有前缀