React 中的Virtual DOM(虚拟DOM)

@一棵菜菜  May 31, 2019

说明

React 中的Virtual DOM(虚拟DOM)模式学习。

Virtual [ˈvɝ​tʃʊəl]

概念

什么是 “Virtual DOM”?

Virtual DOM 是一种由 Javascript 类库基于浏览器 API 实现的概念,一种模式。

在 React 的世界里,术语 “Virtual DOM” 通常与 React 元素关联在一起,因为它们都是代表了用户界面的对象。而 React 也使用一个名为 “fibers” 的内部对象来存放组件树的附加信息。上述二者也被认为是 React 中 “Virtual DOM” 实现的一部分。

什么是 “React Fiber”?

Fiber 是 React 16 中新的协调引擎。它的主要目的是使 Virtual DOM 可以进行增量式渲染

官方文档《Virtual DOM 及内核》

react虚拟DOM原理

在某一时间节点调用 React 的 render() 方法,会创建一棵由 React 元素组成的树。
在下一次 state 或 props 更新时,相同的 render() 方法会返回一棵不同的树。
React 需要基于这两棵树之间的差别来判断如何有效率的更新 UI 以保证当前 UI 与最新的树保持同步。难点在于如何判断新旧两个 JS 对象的最小差异并且实现局部更新 DOM

详细查看我的文章《React中的diffing算法》

React是如何生成虚拟DOM、并渲染成真实DOM的?

推荐阅读《从零开始实现一个React(一):JSX和虚拟DOM》

1. jsx

const element = <h1>Hello, world!</h1>; 它被称为 JSX,是一个 JavaScript 的语法扩展。

Babel 会把 JSX 转译成一个名为 React.createElement() 的普通 JS 函数调用,并且对其取值后得到 JavaScript 对象。

2. 理解React.createElement()生成虚拟DOM

React.createElement(type, [props], [...children]) 创建并返回指定类型的新 React 元素。

  • 第一个类型参数既可以是标签名字符串(如 'div' 或 'span'),也可以是 React 组件 类型 (class 组件或函数组件),或是 React fragment 类型。
  • 第二个参数是一个对象,里面包含了所有的属性,可能包含了className,id等等
  • 从第三个参数开始,就是它的子节点。

3. jsx 被 babel 转换成普通js代码的过程(React.createElement()

试试babel在线编译~

// JSX
   const Name = function (props) {
      return <div>{props.name}</div>;
    };

    const a = (
      <div className="container">
        <Name name="caicai"></Name>
        <h1 title="hello" className="a">
          hello world
        </h1>
        <p>world</p>
      </div>
    );

jsx是语法糖,上面这段代码会被 babel 转换成如下代码:

    const Name = function (props) {
      return React.createElement("div", null, props.name);
    };

    const a = React.createElement(
      "div",
      {
        className: "container",
      },
      React.createElement(Name, {
        name: "caicai",
      }),
      React.createElement(
        "h1",
        {
          title: "hello",
          className: "a",
        },
        "hello world"
      ),
      React.createElement("p", null, "world")
    );

React.createElement() 实际上它创建了一个这样的对象(也就是我们说的虚拟DOM~)

// 注意:这是简化过的结构
 const element = {
      type: "div",
      props: {
        className: "container",
        children: [
          {
            type: "h1",
            props: { title: "hello", className: "a", children: "hello world" },
          },
          { type: "p", props: { children: "world" } },
        ],
      },
    };

这些通过React.createElement()方法生成返回的对象被称为 “React 元素”。它们描述了你希望在屏幕上看到的内容(即记录了这个DOM节点所有的信息)。React 通过读取这些对象,然后使用它们来构建真实的 DOM 以及保持随时更新。而这个记录信息的对象我们称之为虚拟DOM


3. React 如何将虚拟DOM渲染为真实的DOM:通过ReactDOM.render()

想要将一个 React 元素渲染到根 DOM 节点中,只需把它们一起传入 ReactDOM.render(),页面上会展示出 “Hello, world”:

const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));

// 经过转换,这段代码变成了这样
ReactDOM.render(
    React.createElement( 'h1', null, 'Hello, world!' ),
    document.getElementById('root')
);
render的第一个参数实际上接受的是createElement返回的对象,也就是虚拟DOM
而第二个参数则是挂载的目标DOM。总而言之,render()方法的作用就是将虚拟DOM渲染成真实的DOM(递归创建type节点、设置节点属性、创建子节点等)
  1. 当组件的stateprops发生改变时,会重新执行组件中的render()函数,然后react更新DOM,用户就看到新的效果;
  2. 当父组件的render()函数执行时,它的子组件的render()函数也都将被重新执行一次。(不论是否有传数据给字组件。)

假如没有react时,我们如何实现上面的渲染?

实现方法1【不好】

  1. 准备 state 数据
  2. 准备 JSX 模版
  3. render()把数据+模版结合,生成真实的 DOM,再把DOM挂载在页面上显示。
  4. 当state 发生改变
  5. 再用render()把数据+模版结合,生成新的真实的 D0M,替换原始的 D0M 【非常耗性能】

缺陷

第一次生成了一个完整的DOM片段
第二次生成了一个完整的DOM片段
第二次的DOM替换第一次的DOM,是非常耗性能的!


实现方法2【有缺陷】

  1. 准备 state 数据
  2. 准备 JSX 模版
  3. render()把数据+模版结合,生成真实的 DOM,再把DOM挂载在页面上显示。
  4. 当state 发生改变
  5. 数据+模版结合,生成真实的 D0M,但并不直接替换原始的 D0M
  6. 新的 D0M(即js底层的DoucumentFragment,叫文档碎片)和原始的 DOM 做比对,找出差异【DOM对比消耗性能】

    • DOM的比较很耗性能!(但是js中比较js对象不太耗性能)
  7. 比如找出了 input 框发生了变化
  8. 只用新的 D0M 中的 input 元素,替换掉老的 DOM 中的 input 元素——局部更新【节约了性能】

缺陷

性能的提升并不明显(又消耗、又节约)


实现方法3【虚拟DOM,react的实现~】

  1. 准备 state 数据
  2. 准备 JSX 模版
  3. render()把数据+模版结合(JSX),JSX通过React.createElement()生成虚拟 D0M

    • 【损耗了性能(极小)】
    • ['div',{id: 'abc'},['span',{},'hello world']
    • 格式:(component, props, ...children)即[DOM标签,属性集,内容或子节点]
    • 损耗极小的性能,因为用js生成js对象,比js生成DOM元素代价小太多了。
  4. 用虚拟DOM的结构生成真实的 DOM,再把DOM挂载在页面上显示。

    • 比如<div id='abc'><span>hello world</span></div>
  5. 当state 发生改变
  6. render()把数据+模版结合再生成新的虚拟 D0M

    • 【相比方法2中的第5步,现在极大的提升了性能】
    • ['div',{id: 'abc'},['span',{},'bye bye']
  7. 比较新的虚拟 D0M 和原始虚拟 D0M 的区别 ——【diffing算法】(点击阅读文章)

    • 即比较两个js对象,非常不耗性能【所以极大的提升性能】
    • 找到区别是 span 中内容不同了
  8. 直接操作 DOM,改变 span 中的内容

优点

  • 性能提升了
  • 它使得跨端应用得以实现。如React Native写原生应用——得益于虚拟DOM的存在。
理解:如果没有虚拟dom,渲染DOM在浏览器上是没问题的,但在移动端的原生应用里(如安卓、ios等)是没有DOM的概念的,则无法被使用,只能被运行再浏览器里。但是虚拟DOM是js对象,在浏览器、原生里都可以被识别(如在原生里可以让虚拟DOM生成可以识别的组件)。

流程导图:

JSX -> 虚拟DOM(JS对象) -> 真实的DOM -> 挂载渲染

js创建dom元素,底层调用的是web application

小结【牢记】

1.什么是虚拟D0M?

就是一个JS 对象,用它来描述真实 DOM。

2.虚拟DOM为什么能提高性能?

因为减少了对真实DOM的创建和对比,取而代之,创建的都是js对象,对比的也都是js对象,所以 React 底层实现了极大的飞跃。

3. JSX与真实DOM的关系?或:react 生成虚拟DOM并渲染为真实DOM的过程?【牢记】

  1. 我们在代码里编写JSX(不是真实的DOM)
  2. Babel 会把 JSX 转译成一个名为 React.createElement(type, [props], [...children]) 的普通 JS 函数调用;
  3. React.createElement() 创建并返回指定类型的新 React 元素,即生成了虚拟DOM(其实就是JS对象)。【递归】
  4. 执行ReactDOM.render()函数:将虚拟DOM(JS对象)转化生成真实的DOM(html)并挂载。【递归】
  5. 当组件的stateprops发生改变时,会重新执行组件中的render()函数,生成新的虚拟DOM,然后diff,再react更新真实DOM,用户就看到新的效果;
  6. 当父组件的render()函数执行时,它的子组件的render()函数也都将被重新执行一次。(不论是否有传数据给字组件。)
所以实际上,JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖。

例子如下:

    // jsx
    const title = <h1 className="title">Hello</h1>;
    ReactDOM.render(title, document.getElementById("root"));

    // jsx被babel转换成:
    const title = React.createElement("h1", { className: "title" }, "hello");
    ReactDOM.render(
      React.createElement("h1", { className: "title" }, "hello"),
      document.getElementById("root")
    );

    // React.createElement() 返回生成的虚拟DOM 为:
    title = {
      type: "h1",
      props: { className: "title", children: "hello" },
    };

对比

  • vue和react都是虚拟DOM的机制(完全一致的),angularjs是脏值检测方式,虚拟DOM比脏检测要好很多。
  • vue使用的是Object.defineProperty() 进行数据劫持。react是以对象的形式创建虚拟dom然后用diff算法进行比较。这两种效率都很高的,当然相对而言vue更简单。Angular没了解过
更多学习:我的文章《React 虚拟DOM中的Diffing算法》

添加新评论