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. (1) 提供一种方便的工具,使得开发效率得到保证
  2. (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. }
  13.  
  14. //设置虚拟dom的相关属性
  15. this.tagName = tagName;
  16. this.props = props || {};
  17. this.children = children || [];
  18. this.key = props ? props.key : void 666;
  19. var count = 0;
  20. util.each(this.children, function(child, i) {
  21. if (child instanceof VElement) {
  22. count += child.count;
  23. } else {
  24. children[i] = '' + child;
  25. }
  26. count++;
  27. });
  28. this.count = count;
  29. }

通过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>

同样我们可以很方便地根据虚拟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树。

  1. 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算法有两个要点:

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

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

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

  1.  
  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. }
  1.  
  1.  

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

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

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

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

  1. {type:REPLACE,node:newNode}

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

  1. {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. }

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

全面理解虚拟DOM,实现虚拟DOM的更多相关文章

  1. 深入理解react中的虚拟DOM、diff算法

    文章结构: React中的虚拟DOM是什么? 虚拟DOM的简单实现(diff算法) 虚拟DOM的内部工作原理 React中的虚拟DOM与Vue中的虚拟DOM比较 React中的虚拟DOM是什么?   ...

  2. [react] 什么是虚拟dom?虚拟dom比操作原生dom要快吗?虚拟dom是如何转变成真实dom并渲染到页面的?

    壹 ❀ 引 虚拟DOM(Virtual DOM)在前端领域也算是老生常谈的话题了,若你了解过vue或者react一定避不开这个话题,因此虚拟DOM也算是面试中常问的一个点,那么通过本文,你将了解到如下 ...

  3. 什么是虚拟DOM?为啥虚拟DOM可以提升性能?

    现在流行的框架无论是React还是vue,都采用的是虚拟DOM 采用虚拟DOM的好处是,当数据变化的时候,无需像Backbone那样整体重新渲染,而是局部刷新变化部分 所谓虚拟DOM,其实就说用Jav ...

  4. 虚拟dom和真实dom的转化和class解析的顺序

    昨天出去溜了一圈,被问到几个问题回来整理了一下,当被特意问到一看感觉就会的问题,千万要不要急于回答,先想想,因为这往往是一个被忽略的坑(例如class解析顺序)!!! 1.写出虚拟dom和真实dom之 ...

  5. React:关于虚拟DOM(Virtual DOM)

    Virtual DOM 是一个模拟 DOM 树的 JavaScript 对象. React 使用 Virtual DOM 来渲染 UI,当组件状态 state 有更改的时候,React 会自动调用组件 ...

  6. 深入浅出DOM基础——《DOM探索之基础详解篇》学习笔记

    来源于:https://github.com/jawil/blog/issues/9 之前通过深入学习DOM的相关知识,看了慕课网DOM探索之基础详解篇这个视频(在最近看第三遍的时候,准备记录一点东西 ...

  7. 一篇文章教会你如何将DOM转换为virtual DOM

    [一.Virtual DOM简介] Virtual DOM是虚拟节点,它通过Javascript的Object对象模拟DOM中的节点,然后通过特定的render方法将其渲染成真实的DOM节点. 浏览器 ...

  8. 浏览器端-W3School-JavaScript-HTML DOM:HTML DOM Event 对象

    ylbtech-浏览器端-W3School-JavaScript-HTML DOM:HTML DOM Event 对象 1.返回顶部 1. HTML DOM Event 对象 实例 哪个鼠标按钮被点击 ...

  9. Real DOM和 Virtual DOM 的区别?优缺点?

    一.是什么 Real DOM,真实DOM, 意思为文档对象模型,是一个结构化文本的抽象,在页面渲染出的每一个结点都是一个真实DOM结构,如下: Virtual Dom,本质上是以 JavaScript ...

  10. DOM扩展:DOM API的进一步增强[总结篇-下]

    本文承接<DOM扩展:DOM API的进一步增强[总结篇-上]>,继续总结DOM扩展相关的功能和API. 3.6 插入标记 DOM1级中的接口已经提供了向文档中插入内容的接口,但是在给文档 ...

随机推荐

  1. webpack笔记二——entry

    entry是输入目录文件,有三种形式 1.对象键值对形式 entry: { main: './src/script/main.js', b: './src/script/b.js' }, 注意的是输出 ...

  2. ChannelInitializer: 每个channel都new ChannelHandle

    State management 1.业务状态管理-是否登录 A ChannelHandler often needs to store some stateful information. The ...

  3. 冒泡排序快速版(C)

    冒泡排序C语言版:在每轮排序中检查时候有元素位置交换,如果无交换,说明数组元素已经有序,无需继续排序 #include <stdio.h> #include <stdlib.h> ...

  4. OA系统部署短信过程

    安装dotNetFx40_Client_setup.exe插件 安装mysql_installer_community_V5.6.21.1_setup.1415604646.msi数据库 根据数据库版 ...

  5. ComBSTR的使用

    用 CComBSTR 进行编程 Visual Studio .NET 2003   3(共 3)对本文的评价是有帮助 - 评价此主题   ATL 类 CComBSTR 提供对 BSTR 数据类型的包装 ...

  6. Android常用权限permission列表摘录

    一个Android应用程序需要权限才能调用某些android系统的功能:一个android应用也可能被其他应用调用,因此也需要声明调用自身所需要的权限.除了平时常用的权限记得比较熟悉,还有很多的权限一 ...

  7. 【设置】Nginx配置文件具体配置解释

    #定义Nginx运行的用户和用户组 user www www; #nginx进程数,建议设置为等于CPU总核心数. worker_processes 8; #全局错误日志定义类型,[ debug | ...

  8. codeforces 461C

    这题说的是 给了一张长方形的纸 1*n 然后可以按照不同的做法去折这个纸张 他有两种操作,操作1 给了一个pi 点 然后将左边的纸往右边折,第2种操作是给了一个L 和 R 然后计算出 L和R 之间的纸 ...

  9. MySQL重装失败,could not start the service MySQL.Error:0

    MySQL5.5 安装失败现象: mysqld.exe [6132] 中发生了未经处理的 win32 异常 could not start the service MySQL.Error:0 1.在 ...

  10. Linux 远程连接sftp与ftp

    linux sftp远程连接命令 sftp -oPort=60001 root@192.168.0.254 使用-o选项来指定端口号. -oPort=远程端口号 sftp> get /var/w ...