手写VirtualDOM

目前最流行的两大前端框架,React和Vue,都不约而同的借助Virtual DOM技术提高页面的渲染效率。那么,什么是Virtual DOM?它是通过什么方式去提升页面渲染效率的呢?

一、Virtual DOM是什么?

Virtual DOM,虚拟DOM,只是一个简单的JS对象,即用js对象去模拟dom结构。

为什么要模拟DOM结构?

如下:dom节点继承层次复杂,特别是ie中,自身的属性以及继承来的属性特别多,如下div上就有几百个属性

     

  •  一个div上就几百个属性,其次dom继承层级比较复杂,特别是ie中。所以DOM 操作是非常“昂贵”的
  • js对象对比特别快,毕竟js可以写后端,效率还是可以的,所以将 DOM 对比操作放在 JS 层,提高效率、提高重绘性能

通常用三个属性去描述dom结构,比如:标签名(tag)、属性(props)和子元素对象(children),不同的框架对这三个属性的命名会有点差别,但表达的意思是一致的。

<div>
    Hello World!
    <div id="div1" data-idx={1}>first</div>
    <div id="div2">second</div>
</div>

 vm对应结构如下:

{
    "tag": "div",
    "props":{},
    "children": [
        "Hello World!",
        {
            "tag": "div",
            "props":
            {
                "id": "div1",
                "data-idx": 1
            },
            "children": ["first"]
        },
        {
            "tag": "div",
            "props":
            {
                "id": "div2"
            },
            "children": ["second"]
        }
    ]
}

二、Virtual DOM的必要性

如下案例:初次渲染render(data),点击change按钮后修改data数组中的数据,只是修改了两个,但是每次都是“推倒重来”=》?旧的dom结构删掉,插入新的dom结构

以下已经进行了优化,即一次性插入dom节点,而不是一行行tr插入

<body>
    <div id="container"></div>
    <div id="btn-change">change</div>
    <script src="./jquery-3.2.1.js"></script>
    <script>
      var data = [
        {
          name: "张三",
          age: 20,
          address: "北京",
        },
        {
          name: "李四",
          age: 21,
          address: "上海",
        },
        {
          name: "王五",
          age: 22,
          address: "广州",
        },
      ];

      function render(data) {
        var $container = $("#container");
        $container.html("");

        var $table = $("<table>");
        $table.append($("<tr><td>name</td><td>age</td><td>address</td></tr>"));
        data.forEach((element) => {
          $table.append(
            $(
              `<tr><td>${element.name}</td><td>${element.age}</td><td>${element.address}</td></tr>`
            )
          );
        });
        $container.append($table);
      }

      $("#btn-change").click(function () {
        data[1].age = 30;
        data[2].address = "深圳";
        render(data);
      });

      render(data);
    </script>
  </body>

思考:只是修改了两个数据,没必要推到重来,毕竟dom操作是很昂贵的,若只是更新变化的dom,那么性能会提升很多

snabbdom:https://github.com/snabbdom/snabbdom

<body>
    <div id="container"></div>
    <div id="btn-change">change</div>

    <script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-class.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-props.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-style.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-eventlisteners.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/h.js"></script>
    <script>
      var snabbdom = window.snabbdom;
      var patch = snabbdom.init([
        snabbdom_class,
        snabbdom_props,
        snabbdom_style,
        snabbdom_eventlisteners,
      ]);
      var h = snabbdom.h;

      var data = [
        {
          name: "姓名",
          age: "年龄",
          address: "地址",
        },
        {
          name: "张三",
          age: 20,
          address: "北京",
        },
        {
          name: "李四",
          age: 21,
          address: "上海",
        },
        {
          name: "王五",
          age: 22,
          address: "广州",
        },
      ];

      var vnode;
      var container = document.getElementById("container");
      function render(data) {
        var newVnode = h(
          "table",
          {},
          data.map(function (item) {
            var tds = [];

            for (let i in item) {
              if (item.hasOwnProperty(i)) {
                tds.push(h("td", {}, item[i] + ""));
              }
            }
            return h("tr", {}, tds);
          })
        );

        if (vnode) {
          patch(vnode, newVnode);
        } else {
          patch(container, newVnode);
        }
        vnode = newVnode;
      }

      document.getElementById("btn-change").onclick = function () {
        data[1].age = 30;
        data[2].address = "深圳";
        render(data);
      };

      render(data);
    </script>
  </body>

如下当点击按钮时,只是更新了变化的dom而不像jquery那样“推倒重来”

Virtual DOM优点:

  • Virtual DOM 最大的特点是将页面的状态抽象为 JS 对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。如 React 就借助 Virtual DOM 实现了服务端渲染、浏览器渲染和移动端渲染等功能。
  • 在进行页面更新的时候,借助Virtual DOM,DOM 元素的改变可以在内存中进行比较,再结合框架的事务机制将多次比较的结果合并后一次性更新到页面,从而有效地减少页面渲染的次数,提高渲染效率。

如下页面的更新一般会经过几个阶段:

从上面可以看出页面的呈现会分以下3个阶段:

  • JS计算
  • 生成渲染树
  • 绘制页面

JS计算用了935毫秒,生成渲染树143毫秒,绘制60毫秒。如果能有效的减少生成渲染树和绘制所花的时间,更新页面的效率也会随之提高。通过Virtual DOM的比较,我们可以将多个操作合并成一个批量的操作,从而减少dom重排的次数,进而缩短了生成渲染树和绘制所花的时间。

三、如何实现Virtual DOM与真实DOM的映射

借助JSX编译器,可以将文件中的HTML转化成函数的形式,然后再利用这个函数生成Virtual DOM。看下面这个例子:

//index.js
function view() {
  return (
    <div>
      Hello World!
      <div id="div1" data-ids="{1}">
        first
      </div>
      <div id="div2">second</div>
    </div>
  );
}

通过babel的plugin(transform-react-jsx)编译后,可以生成如下代码:babel ./src/index.js -o ./public/vm_bundle.js(babel ./src/index.js --out-file ./public/vm_bundle.js)

//vm_bundle.js
function view() {
  return h(
    "div",
    null,
    "Hello World!",
    h("div", { id: "div1", "data-ids": "{1}" }, "first"),
    h("div", { id: "div2" }, "second")
  );
}

这里的h是一个函数,可以起任意的名字-类似于snabbdom的h函数。这个名字通过babel进行配置:


// 安装npm包: npm install --save-dev babel-cli babel-plugin-transform-react-jsx
// babel-cli 是babel的命令行工具,需要将原始的 .js或.jsx 文件编译

// .babelrc
{
  "plugins": [
    [
      "transform-react-jsx",
      {
        "pragma": "h"
      }
    ]
  ]
}

接下来,只需要定义h函数,就能构造出Virtual DOM:

//h函数:生成vnode
function flatten(children) {
  return [].concat.apply([], children);
}

function h(tag, props, ...children) {
  return {
    tag,
    props: props || {},
    children: flatten(children) || [],
  };
}

初次渲染,生成vdom后,要基于Virtual DOM,生成真实的DOM--相当于实现snabbdom的patch函数:

//patch函数:vnode=》真实dom
function setProps(element, props) {
  for (key in props) {
    if (props.hasOwnProperty(key)) {
      element.setAttribute(key, props[key]);
    }
  }
}

function createElement(vdom) {
    const t = typeof vdom;
    if (t === 'string' || t === 'number') {
        return document.createTextNode(vdom);
    }

    const {tag, props, children} = vdom;

    // 1. 创建元素
    const element = document.createElement(tag);
    
    // 2. 属性赋值
    setProps(element, props);
    
    // 3. 创建子元素
    // appendChild在执行的时候,会检查当前的this是不是dom对象,因此要bind一下
    children.map(createElement).forEach(element.appendChild.bind(element));
    //children.map(createElement).forEach((child) => {
    // element.appendChild(child);
    //});
    
    return element;
}

最后,将生成好的dom挂载到指定的节点上:

//真实dom插入页面
function renderDOM(vdom, container) {
  container.appendChild(createElement(vdom));
}

renderDOM(view(), document.getElementById("root"));

如下可以通过一个html来显示最终成果:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="app"></div>
    <script src="vm_bundle.js"></script>
</body>
</html>

展示效果如下:

五、总结

本文介绍了Virtual DOM的基本概念,并讲解了如何利用JSX编译HTML标签,然后生成Virtual DOM,进而创建真实dom的过程。

项目源码:
https://github.com/qingye/VirtualDOM-Study/tree/master/VirtualDOM-Study-01

猜你喜欢

转载自blog.csdn.net/CamilleZJ/article/details/116992966