现在的流行框架,无论React还是Vue,都采用虚拟DOM。

好处就是,当我们数据变化时,无需像Backbone那样整体重新渲染,而是局部刷新变化部分,如下组件模版:

<ul class="list">
<li>item1</li>
<li>item2</li>
</ul>

当页面中item2变为item3时,如Backbone一样的MVC框架就会将ul这个模块整体刷新,而如果我们采用虚拟DOM来实现,就会只将'item2'这个文本节点变为'item3'文本节点。

初看虚拟DOM,感觉很玄乎,但是剥开它华丽的外衣,也就那样:

1. 通过JavaScript来构建虚拟的DOM树结构,并将其呈现到页面中;

2. 当数据改变,引起DOM树结构发生改变,从而生成一颗新的虚拟DOM树,将其与之前的DOM对比,将变化部分应用到真实的DOM树中,即页面中。

通过上面的介绍,下面,我们就来实现一个简单的虚拟DOM,并将其与真实的DOM关联。

一、构建虚拟DOM

虚拟DOM,其实就是用JavaScript对象来构建DOM树,如上ul组件模版,其树形结构如下:

通过JavaScript,我们可以很容易构建它,如下:

var elem = Element({
tagName: 'ul',
props: {'class': 'list'},
children: [
Element({tagName: 'li', children: ['item1']}),
Element({tagName: 'li', children: ['item2']})
]
});

note:Element为一个构造函数,返回一个Element对象。为了更清晰的呈现虚拟DOM结构,我们省略了new,而在Element中实现。

看了上面JavaScript构建的虚拟DOM树,不难实现Element构造函数,如下:

/*
* @Params:
* tagName(string)(requered)
* props(object)(optional)
* children(array)(optional)
* */
function Element({tagName, props, children}){
if(!(this instanceof Element)){
return new Element({tagName, props, children})
}
this.tagName = tagName;
this.props = props || {};
this.children = children || [];
}

好了,通过Element我们可以任意地构建虚拟DOM树了。但是有个问题,虚拟的终归是虚拟的,我们得将其呈现到页面中,不然,没卵用。。

怎么呈现呢?

从上面得知,这是一颗树嘛,那我们就通过遍历,逐个节点地创建真实DOM节点:

  1. createElement;

  2. createTextNode.

怎么遍历呢?

因为这是一颗树嘛,对于树形结构无外乎两种遍历:

  1. 深度优先遍历(DFS)

  2. 广度优先遍历(BFS)

下面我们就来回顾下《数据结构》中,这两种遍历的思想:

1. DFS利用栈来遍历数据,如下:

2. BFS利用队列来遍历数据,如下:

针对实际情况,我们得采用DFS,为什么呢?

那尼,还是这种疑问?!!因为我们得将子节点append到父节点中,如果采用BFS搞毛线啊!!

好了,那我们采用DFS,就来实现一个render函数吧,如下:

Element.prototype.render = function(){
var el = document.createElement(this.tagName),
props = this.props,
propName,
propValue;
for(propName in props){
propValue = props[propName];
el.setAttribute(propName, propValue);
}
this.children.forEach(function(child){
var childEl = null;
if(child instanceof Element){
childEl = child.render();
}else{
childEl = document.createTextNode(child);
}
el.appendChild(childEl);
});
return el;
};

此时,我们就可以轻松地将虚拟DOM呈现到指定真实DOM中啦。假设,我们将上诉ul虚拟DOM呈现到页面body中,如下:

var elem = Element({
tagName: 'ul',
props: {'class': 'list'},
children: [
Element({tagName: 'li', children: ['item1']}),
Element({tagName: 'li', children: ['item2']})
]
});
document.querySelector('body').appendChild(elem.render());
二、处理DOM更新

在前一小结,我们成功地实现了虚拟DOM,并将其转化为真实DOM,呈现在页面中。

接下来,我们就处理当DOM更新时,怎样通过新旧虚拟DOM对比,然后将变化部分更新到真实DOM中的问题。

DOM更新,无外乎四种情况,如下:

  1. 新增节点;

  2. 删除节点;

  3. 替换节点;

  4. 父节点相同,对比子节点.

好了,需求了解,开始我们的表演。

毫无疑问,遍历DOM树仍然采用DFS遍历。

因为我们要将变化的节点更新到真实DOM中,所以还得传入真实的DOM根节点,并且真实的DOM节点与虚拟的DOM节点,树形结构一致,故通过标记可以记录节点变化位置,如下:

实现函数如下:

function updateElement($root, newElem, oldElem, index = 0) {
if (!oldElem){
$root.appendChild(newElem.render());
} else if (!newElem) {
$root.removeChild($root.childNodes[index]);
} else if (changed(newElem, oldElem)) {
if (typeof newElem === 'string') {
$root.childNodes[index].textContent = newElem;
} else {
$root.replaceChild(newElem.render(), $root.childNodes[index]);
}
} else if (newElem.tagName) {
let newLen = newElem.children.length;
let oldLen = oldElem.children.length;
for (let i = 0; i < newLen || i < oldLen; i++) {
updateElement($root.childNodes[index], newElem.children[i], oldElem.children[i], i)
}
}
}

其中的changed方法,简单实现如下:

function changed(elem1, elem2) {
return (typeof elem1 !== typeof elem2) ||
(typeof elem1 === 'string' && elem1 !== elem2) ||
(elem1.type !== elem2.type);
}

好了,一个简单的虚拟DOM就实现了。

三、效果展示

通过JS构建一颗虚拟DOM(如上诉ul),并将其呈现到页面中,然后替换其子节点,动态更新到真实DOM中,如下:

<body>
<button id="refresh">refresh element</button>
<div id="root"></div>
<script src="./virtualDom.js"></script>
<script>
var elem = Element({
tagName: 'ul',
props: {'class': 'list'},
children: [
Element({tagName: 'li', children: ['item1']}),
Element({tagName: 'li', children: ['item2']})
]
});
var newElem = Element({
tagName: 'ul',
props: {'class': 'list'},
children: [
Element({tagName: 'li', children: ['item1']}),
Element({tagName: 'li', children: ['hahaha']})
]
});
var $root = document.querySelector('#root');
var $refresh = document.querySelector('#refresh');
updateElement($root, elem);
$refresh.addEventListener('click', () => {
updateElement($root, newElem, elem);
});
</script>
</body>

效果如下:

四、拓展阅读

[1]. 树结构之JavaScript

[2]. virtual dom

[3]. 虚拟DOM

实现一个简单的虚拟DOM的更多相关文章

  1. 实现一个简单的虚拟demo算法

    假如现在你需要写一个像下面一样的表格的应用程序,这个表格可以根据不同的字段进行升序或者降序的展示. 这个应用程序看起来很简单,你可以想出好几种不同的方式来写.最容易想到的可能是,在你的 JavaScr ...

  2. 一个简单的创建dom的函数

    var  regName = /^(div|a|p|ul|li|input|select|document|body|iframe)$/;function createDom(name, obj) { ...

  3. 如何快速实现一个虚拟 DOM 系统

    虚拟 DOM 是目前主流前端框架的技术核心之一,本文阐述如何实现一个简单的虚拟 DOM 系统. 为什么需要虚拟 DOM? 虚拟 DOM 就是一棵由虚拟节点组成的树,这棵树展现了真实 DOM 的结构.这 ...

  4. 手写一个虚拟DOM库,彻底让你理解diff算法

    所谓虚拟DOM就是用js对象来描述真实DOM,它相对于原生DOM更加轻量,因为真正的DOM对象附带有非常多的属性,另外配合虚拟DOM的diff算法,能以最少的操作来更新DOM,除此之外,也能让Vue和 ...

  5. React virtual DOM explained in simple English/简单语言解释React的虚拟DOM

    初学React,其中一个很重要的概念是虚拟DOM,看了一篇文章,顺带翻译一下. If you are using React or learning React, you must have hear ...

  6. JavaScript是如何工作的:编写自己的Web开发框架 + React及其虚拟DOM原理

    这是专门探索 JavaScript 及其所构建的组件的系列文章的第 19 篇. 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述! Jav ...

  7. vue核心之虚拟DOM

    一.前言 虚拟DOM概念随着react的诞生而诞生,由facebook提出,其卓越的性能很快得到广大开发者的认可:继react之后vue2.0也在其核心引入了虚拟DOM的概念,本文将以vue2.0使用 ...

  8. react中虚拟dom的diff算法

    .state 数据 .jsx模板 .生成虚拟dom(虚拟DOM就是一个js对象,用它来描述真实DOM) ['div', {id:'abc'}, ['span', {}, 'hello world']] ...

  9. 从零开始学虚拟DOM

    此文主要翻译自:Building a Simple Virtual DOM from Scratch,看原文的同学请直达! 此文是作者在一次现场编程演讲时现场所做的,有关演讲的相关资料我们也可以在原英 ...

随机推荐

  1. linux之无名管道

    1.查看命令: man 2 pipe 2.头文件:#include <unistd.h> 3.函数原型: int pipe(int pipefd[2]); a.pipefd[2] :无名管 ...

  2. 错误Deprecated: mysql_connect(): The mysql extension is deprecated and will be removed in the future:

    今天写PHP代码,遇到了这个非常不友好的报错(Deprecated: mysql_connect(): The mysql extension is deprecated and will be re ...

  3. 怎样查看Jdk是32位还是64位

    怎样查看Jdk是32位还是64位.. ------------- --------------- ------------- 其实还可以这样看哦.. --------------- --------- ...

  4. VS2015 安装nuget离线包nupkg文件

    最近在做项目进度管理时,想通过安装net.sf.mpxj-for-csharp包读取.mpp格式文件,通过Nuget在线安装时,出现以下情况,无法安装,故开启离线安装道路. 离线安装步骤如下: 一.下 ...

  5. 自动化的基于TypeScript的HTML5游戏开发

    自动化的开发流程 在HTML5游戏开发或者说在Web客户端开发中,对项目代码进行修改之后,一般来说,需要手动刷新浏览器来查看代码修改后运行结果.这种手动的方式费时费力,降低了开发效率.另外,如果我们使 ...

  6. Andrew Ng机器学习课程笔记--week11(图像识别&总结划重点)

    一.内容概要 Photo OCR Problem Decription and pipeline(问题描述和流程图) Sliding Windows(滑动窗口) Getting Lots of Dat ...

  7. Firefox插件推荐

    介绍一下我常用的几个Firefox插件,不一定适合所有人. 图中所示4个插件是我仅有的4个addons,除了stylish,其他几个已经伴随我多年. 1.Adblock Plus(强烈推荐) 广告拦截 ...

  8. MIPI DSI转LVDS芯片方案TC358775XBG

    型号:TC358775XBG功能:MIPI转LVDS通信方式:IIC/MIPI Command mode分辨率:1920*1080电源:3.3/1.8/1.2封装形式:BGA64深圳长期现货 ,提供技 ...

  9. Webservice优缺点总结

    优点: 1 .采用xml支持跨平台远程调用. 2.基于http的soap协议,可跨越防火墙. (因为SOAP一般使用HTTP协议,而服务器的这个协议一般都是开放的,而且是可以穿过防火墙的) 3.支持面 ...

  10. mysql 用多次查询代替一次复杂join查询的优点分析

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt344 多高性能的应用都会对关联查询进行分解.简单地,可以对每一个表进行一次单 ...