• 豌豆资源网
  • 开引网企业服务
  • 服务外包网
  • 全面理解虚拟DOM,实现虚拟DOM

    最近一两年前端最火的技术莫过于ReactJS,即便你没用过也该听过,ReactJS由业界顶尖的互联网公司facebook提出,其本身有很多先进的设计思路,比如页面UI组件化、虚拟DOM等。本文将带你解开虚拟DOM的神秘面纱,不仅要理解其原理,而且要实现一个基本可用的虚拟DOM。

     

    1.为什么需要虚拟DOM

    DOM是很慢的,其元素非常庞大,页面的性能问题鲜有由JS引起的,大部分都是由DOM操作引起的。如果对前端工作进行抽象的话,主要就是维护状态和更新视图;而更新视图和维护状态都需要DOM操作。其实近年来,前端的框架主要发展方向就是解放DOM操作的复杂性。

    在jQuery出现以前,我们直接操作DOM结构,这种方法复杂度高,兼容性也较差;有了jQuery强大的选择器以及高度封装的API,我们可以更方便的操作DOM,jQuery帮我们处理兼容性问题,同时也使DOM操作变得简单;但是聪明的程序员不可能满足于此,各种MVVM框架应运而生,有angularJS、avalon、vue.js等,MVVM使用数据双向绑定,使得我们完全不需要操作DOM了,更新了状态视图会自动更新,更新了视图数据状态也会自动更新,可以说MMVM使得前端的开发效率大幅提升,但是其大量的事件绑定使得其在复杂场景下的执行性能堪忧;有没有一种兼顾开发效率和执行效率的方案呢?ReactJS就是一种不错的方案,虽然其将JS代码和HTML代码混合在一起的设计有不少争议,但是其引入的Virtual DOM(虚拟DOM)却是得到大家的一致认同的。

     

    2.理解虚拟DOM

    虚拟的DOM的核心思想是:对复杂的文档DOM结构,提供一种方便的工具,进行最小化地DOM操作。这句话,也许过于抽象,却基本概况了虚拟DOM的设计思想

    (1) 提供一种方便的工具,使得开发效率得到保证
    (2) 保证最小化的DOM操作,使得执行效率得到保证
    

    (1).用JS表示DOM结构

    DOM很慢,而javascript很快,用javascript对象可以很容易地表示DOM节点。DOM节点包括标签、属性和子节点,通过VElement表示如下。

     1 //虚拟dom,参数分别为标签名、属性对象、子DOM列表
     2 var VElement = function(tagName, props, children) {
     3     //保证只能通过如下方式调用:new VElement
     4     if (!(this instanceof VElement)) {
     5         return new VElement(tagName, props, children);
     6     }
     7 
     8     //可以通过只传递tagName和children参数
     9     if (util.isArray(props)) {
    10         children = props;
    11         props = {};
    12     }

     

     1   //设置虚拟dom的相关属性
     2     this.tagName = tagName;
     3     this.props = props || {};
     4     this.children = children || [];
     5     this.key = props ? props.key : void 666;
     6     var count = 0;
     7     util.each(this.children, function(child, i) {
     8         if (child instanceof VElement) {
     9             count += child.count;
    10         } else {
    11             children[i] = '' + child;
    12         }
    13         count++;
    14     });
    15     this.count = count;
    16 }

     

    通过VElement,我们可以很简单地用javascript表示DOM结构。比如

    1 var vdom = velement('div', { 'id': 'container' }, [
    2     velement('h1', { style: 'color:red' }, ['simple virtual dom']),
    3     velement('p', ['hello world']),
    4     velement('ul', [velement('li', ['item #1']), velement('li', ['item #2'])]),
    5 ]);

     

    上面的javascript代码可以表示如下DOM结构:

    1 <div id="container">
    2     <h1 style="color:red">simple virtual dom</h1>
    3     <p>hello world</p>
    4     <ul>
    5         <li>item #1</li>
    6         <li>item #2</li>
    7     </ul>   
    8 </div>

    办公资源网址导航 https://www.wode007.com

    同样我们可以很方便地根据虚拟DOM树构建出真实的DOM树。具体思路:根据虚拟DOM节点的属性和子节点递归地构建出真实的DOM树。见如下代码:

     1 VElement.prototype.render = function() {
     2     //创建标签
     3     var el = document.createElement(this.tagName);
     4     //设置标签的属性
     5     var props = this.props;
     6     for (var propName in props) {
     7         var propValue = props[propName]
     8         util.setAttr(el, propName, propValue);
     9     }
    10 
    11     //依次创建子节点的标签
    12     util.each(this.children, function(child) {
    13         //如果子节点仍然为velement,则递归的创建子节点,否则直接创建文本类型节点
    14         var childEl = (child instanceof VElement) ? child.render() : document.createTextNode(child);
    15         el.appendChild(childEl);
    16     });
    17 
    18     return el;
    19 }

     

    对一个虚拟的DOM对象VElement,调用其原型的render方法,就可以产生一颗真实的DOM树。

        vdom.render();
    

    既然我们可以用JS对象表示DOM结构,那么当数据状态发生变化而需要改变DOM结构时,我们先通过JS对象表示的虚拟DOM计算出实际DOM需要做的最小变动,然后再操作实际DOM,从而避免了粗放式的DOM操作带来的性能问题。

    (2).比较两棵虚拟DOM树的差异

    在用JS对象表示DOM结构后,当页面状态发生变化而需要操作DOM时,我们可以先通过虚拟DOM计算出对真实DOM的最小修改量,然后再修改真实DOM结构(因为真实DOM的操作代价太大)。

    如下图所示,两个虚拟DOM之间的差异已经标红:

     

    为了便于说明问题,我当然选取了最简单的DOM结构,两个简单DOM之间的差异似乎是显而易见的,但是真实场景下的DOM结构很复杂,我们必须借助于一个有效的DOM树比较算法。

    设计一个diff算法有两个要点:

    如何比较两个两棵DOM树
    如何记录节点之间的差异
    

    <1> 如何比较两个两棵DOM树

    计算两棵树之间差异的常规算法复杂度为O(n3),一个文档的DOM结构有上百个节点是很正常的情况,这种复杂度无法应用于实际项目。针对前端的具体情况:我们很少跨级别的修改DOM节点,通常是修改节点的属性、调整子节点的顺序、添加子节点等。因此,我们只需要对同级别节点进行比较,避免了diff算法的复杂性。对同级别节点进行比较的常用方法是深度优先遍历:

    1 function diff(oldTree, newTree) {
    2     //节点的遍历顺序
    3     var index = 0; 
    4     //在遍历过程中记录节点的差异
    5     var patches = {}; 
    6     //深度优先遍历两棵树
    7     dfsWalk(oldTree, newTree, index, patches); 
    8     return patches; 
    9 }

     

    <2>如何记录节点之间的差异

    由于我们对DOM树采取的是同级比较,因此节点之间的差异可以归结为4种类型:

    修改节点属性, 用PROPS表示
    修改节点文本内容, 用TEXT表示
    替换原有节点, 用REPLACE表示
    调整子节点,包括移动、删除等,用REORDER表示
    

    对于节点之间的差异,我们可以很方便地使用上述四种方式进行记录,比如当旧节点被替换时:

    {type:REPLACE,node:newNode}
    

    而当旧节点的属性被修改时:

    {type:PROPS,props: newProps}
    

    在深度优先遍历的过程中,每个节点都有一个编号,如果对应的节点有变化,只需要把相应变化的类别记录下来即可。下面是具体实现:

     1 function dfsWalk(oldNode, newNode, index, patches) {
     2     var currentPatch = [];
     3     if (newNode === null) {
     4         //依赖listdiff算法进行标记为删除
     5     } else if (util.isString(oldNode) && util.isString(newNode)) {
     6         if (oldNode !== newNode) {
     7             //如果是文本节点则直接替换文本
     8             currentPatch.push({
     9                 type: patch.TEXT,
    10                 content: newNode
    11             });
    12         }
    13     } else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
    14         //节点类型相同
    15         //比较节点的属性是否相同
    16         var propsPatches = diffProps(oldNode, newNode);
    17         if (propsPatches) {
    18             currentPatch.push({
    19                 type: patch.PROPS,
    20                 props: propsPatches
    21             });
    22         }
    23         //比较子节点是否相同
    24         diffChildren(oldNode.children, newNode.children, index, patches, currentPatch);
    25     } else {
    26         //节点的类型不同,直接替换
    27         currentPatch.push({ type: patch.REPLACE, node: newNode });
    28     }
    29 
    30     if (currentPatch.length) {
    31         patches[index] = currentPatch;
    32     }
    33 }

     

    比如对上文图中的两颗虚拟DOM树,可以用如下数据结构记录它们之间的变化:

    1 var patches = {
    2         1:{type:REPLACE,node:newNode}, //h1节点变成h5
    3         5:{type:REORDER,moves:changObj} //ul新增了子节点li
    4     }

     

    (3).对真实DOM进行最小化修改

    通过虚拟DOM计算出两颗真实DOM树之间的差异后,我们就可以修改真实的DOM结构了。上文深度优先遍历过程产生了用于记录两棵树之间差异的数据结构patches, 通过使用patches我们可以方便对真实DOM做最小化的修改。

     1 //将差异应用到真实DOM
     2 function applyPatches(node, currentPatches) {
     3     util.each(currentPatches, function(currentPatch) {
     4         switch (currentPatch.type) {
     5             //当修改类型为REPLACE时
     6             case REPLACE:
     7                 var newNode = (typeof currentPatch.node === 'String')
     8                  ? document.createTextNode(currentPatch.node) 
     9                  : currentPatch.node.render();
    10                 node.parentNode.replaceChild(newNode, node);
    11                 break;
    12             //当修改类型为REORDER时
    13             case REORDER:
    14                 reoderChildren(node, currentPatch.moves);
    15                 break;
    16             //当修改类型为PROPS时
    17             case PROPS:
    18                 setProps(node, currentPatch.props);
    19                 break;
    20             //当修改类型为TEXT时
    21             case TEXT:
    22                 if (node.textContent) {
    23                     node.textContent = currentPatch.content;
    24                 } else {
    25                     node.nodeValue = currentPatch.content;
    26                 }
    27                 break;
    28             default:
    29                 throw new Error('Unknow patch type ' + currentPatch.type);
    30         }
    31     });
    32 }

     

     

    posted @ 2020-06-03 20:17  前端一点红  阅读(2577)  评论(0编辑  收藏  举报
  • 乐游资源网
  • 热爱资源网
  • 灵活用工代发薪平台
  • 企服知识
  • 355软件知识