全面理解虚拟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结构:

simple virtual dom

hello world

  • item #1
  • item #2
  • 同样我们可以很方便地根据虚拟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之间的差异已经标红:

    为了便于说明问题,我当然选取了最简单的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);

    }

    });

    }

    ©著作权归作者所有,转载或内容合作请联系作者
    • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
      沈念sama阅读 203,362评论 5 477
    • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
      沈念sama阅读 85,330评论 2 381
    • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
      开封第一讲书人阅读 150,247评论 0 337
    • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
      开封第一讲书人阅读 54,560评论 1 273
    • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
      茶点故事阅读 63,580评论 5 365
    • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
      开封第一讲书人阅读 48,569评论 1 281
    • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
      沈念sama阅读 37,929评论 3 395
    • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
      开封第一讲书人阅读 36,587评论 0 258
    • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
      沈念sama阅读 40,840评论 1 297
    • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
      茶点故事阅读 35,596评论 2 321
    • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
      茶点故事阅读 37,678评论 1 329
    • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
      沈念sama阅读 33,366评论 4 318
    • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
      茶点故事阅读 38,945评论 3 307
    • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
      开封第一讲书人阅读 29,929评论 0 19
    • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
      开封第一讲书人阅读 31,165评论 1 259
    • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
      沈念sama阅读 43,271评论 2 349
    • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
      茶点故事阅读 42,403评论 2 342

    推荐阅读更多精彩内容