zoukankan      html  css  js  c++  java
  • 全面理解虚拟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表示如下。

    //虚拟dom,参数分别为标签名、属性对象、子DOM列表
    var VElement = function(tagName, props, children) {
        //保证只能通过如下方式调用:new VElement
        if (!(this instanceof VElement)) {
            return new VElement(tagName, props, children);
        }
    
        //可以通过只传递tagName和children参数
        if (util.isArray(props)) {
            children = props;
            props = {};
        }
    
        //设置虚拟dom的相关属性
        this.tagName = tagName;
        this.props = props || {};
        this.children = children || [];
        this.key = props ? props.key : void 666;
        var count = 0;
        util.each(this.children, function(child, i) {
            if (child instanceof VElement) {
                count += child.count;
            } else {
                children[i] = '' + child;
            }
            count++;
        });
        this.count = count;
    }

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

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

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

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

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

    VElement.prototype.render = function() {
        //创建标签
        var el = document.createElement(this.tagName);
        //设置标签的属性
        var props = this.props;
        for (var propName in props) {
            var propValue = props[propName]
            util.setAttr(el, propName, propValue);
        }
    
        //依次创建子节点的标签
        util.each(this.children, function(child) {
            //如果子节点仍然为velement,则递归的创建子节点,否则直接创建文本类型节点
            var childEl = (child instanceof VElement) ? child.render() : document.createTextNode(child);
            el.appendChild(childEl);
        });
    
        return el;
    }

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

    vdom.render();

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

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

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

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

    virtual dom

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

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

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

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

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

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

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

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

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

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

    {type:REPLACE,node:newNode}

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

    {type:PROPS,props: newProps}

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

    function dfsWalk(oldNode, newNode, index, patches) {
        var currentPatch = [];
        if (newNode === null) {
            //依赖listdiff算法进行标记为删除
        } else if (util.isString(oldNode) && util.isString(newNode)) {
            if (oldNode !== newNode) {
                //如果是文本节点则直接替换文本
                currentPatch.push({
                    type: patch.TEXT,
                    content: newNode
                });
            }
        } else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
            //节点类型相同
            //比较节点的属性是否相同
            var propsPatches = diffProps(oldNode, newNode);
            if (propsPatches) {
                currentPatch.push({
                    type: patch.PROPS,
                    props: propsPatches
                });
            }
            //比较子节点是否相同
            diffChildren(oldNode.children, newNode.children, index, patches, currentPatch);
        } else {
            //节点的类型不同,直接替换
            currentPatch.push({ type: patch.REPLACE, node: newNode });
        }
    
        if (currentPatch.length) {
            patches[index] = currentPatch;
        }
    }

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

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

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

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

    //将差异应用到真实DOM
    function applyPatches(node, currentPatches) {
        util.each(currentPatches, function(currentPatch) {
            switch (currentPatch.type) {
                //当修改类型为REPLACE时
                case REPLACE:
                    var newNode = (typeof currentPatch.node === 'String')
                     ? document.createTextNode(currentPatch.node) 
                     : currentPatch.node.render();
                    node.parentNode.replaceChild(newNode, node);
                    break;
                //当修改类型为REORDER时
                case REORDER:
                    reoderChildren(node, currentPatch.moves);
                    break;
                //当修改类型为PROPS时
                case PROPS:
                    setProps(node, currentPatch.props);
                    break;
                //当修改类型为TEXT时
                case TEXT:
                    if (node.textContent) {
                        node.textContent = currentPatch.content;
                    } else {
                        node.nodeValue = currentPatch.content;
                    }
                    break;
                default:
                    throw new Error('Unknow patch type ' + currentPatch.type);
            }
        });
    }

     原文来自于:http://blog.csdn.net/yczz/article/details/51292169

  • 相关阅读:
    DRUPAL-PSA-CORE-2014-005 && CVE-2014-3704 Drupal 7.31 SQL Injection Vulnerability /includes/database/database.inc Analysis
    WDCP(WDlinux Control Panel) mysql/add_user.php、mysql/add_db.php Authentication Loss
    Penetration Testing、Security Testing、Automation Testing
    Tomcat Server Configuration Automation Reinforcement
    Xcon2014 && Geekpwn2014
    phpMyadmin /scripts/setup.php Remote Code Injection && Execution CVE-2009-1151
    Linux System Log Collection、Log Integration、Log Analysis System Building Learning
    The Linux Process Principle,NameSpace, PID、TID、PGID、PPID、SID、TID、TTY
    Windows Management Instrumentation WMI Security Technology Learning
    IIS FTP Server Anonymous Writeable Reinforcement, WEBDAV Anonymous Writeable Reinforcement(undone)
  • 原文地址:https://www.cnblogs.com/goldlong/p/7975755.html
Copyright © 2011-2022 走看看