虚拟DOM解析及其在框架里的应用


浏览器是怎样解析HTML并且绘出整个页面的



上图为webkit引擎浏览器的处理流程,如上图大致分为4大步:

第一步,HTML解析器分析html,构建一颗DOM树;

第二步,CSS解析器会分析外联的css文件和内联的一些样式,创建一个页面的样式表;

第三步,将DOM树和样式表关联起来,创建一颗Render树。这一过程又被称为Attachment,每个DOM节点上都有一个attach方法,会接收对应的样式表,返回一个render对象。这些render对象最终会结合成一个render tree;

第四步,有了render tree后,浏览器就可以为render tree上的每个节点在屏幕上分配一个精确的位置坐标,然后各个节点会调用自身的paint方法结合坐标信息,在浏览器中绘制中整个页面了。

回流(reflow)和重绘

回流和重绘都是浏览器自身的行为

回流:当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。

重绘:当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。

减少回流的办法(不是这次讨论重点):比如cssText、避免使用会强制reflush队列的属性等等

手动操作DOM带来的性能忧患

在使用原生的js api或者jquery等一些方法直接去操作dom的时候,可能会引起页面的reflow,而页面的回流所带来的代价是非常昂贵的。频繁的去操作dom,会引起页面的卡顿,影响用户的体验。

这里打印一个最简单的真实DOM节点里面的属性

虚拟DOM就是为了解决频繁操作DOM的性能问题创造出来的。

例如:

如果使用原生api去操作一个会导致回流的DOM操作10次,那么浏览器会每次都会重新走一次上面的全流程,包括一些没变化的位置计算。

而虚拟DOM不会立即去操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attach到DOM树上,通知浏览器去执行绘制工作,这样可以避免大量的无谓的计算量。

virtual dom 是什么

这是一段真实的DOM tree结构

<div id="container">
<p>Real DOM</p>
<ul>
<li class="item">Item 1</li>
<li class="item">Item 2</li>
<li class="item">Item 3</li>
</ul>
</div>

如果用js对象来模拟上述的DOM Tree

let virtualDomTree = CreateElement('div', { id: 'container' }, [
CreateElement('p', {}, ['Virtual DOM']),
CreateElement('ul', {}, [
CreateElement('li', { class: 'item' }, ['Item 1']),
CreateElement('li', { class: 'item' }, ['Item 2']),
CreateElement('li', { class: 'item' }, ['Item 3']),
]),
]); let root = virtualDomTree.render(); //转换为一个真正的dom结构或者dom fragment
document.getElementById('virtualDom').appendChild(root);

这样的好处,避免了因直接去修改真实的dom而带来的性能隐患。可以先把页面的一些改动反应到这个虚拟dom对象上,等更新完后再一次统一去把变化同步到真实的dom中。

下面是CreateElement的实现方法

function CreateElement(tagName, props, children) {
if (!(this instanceof CreateElement)) {
return new CreateElement(tagName, props, children);
} this.tagName = tagName;
this.props = props || {};
this.children = children || [];
this.key = props ? props.key : undefined;
}

render方法

CreateElement.prototype.render = function() {
let el = document.createElement(this.tagName);
let props = this.props; for (let propName in props) {
setAttr(el, propName, props[propName]);
} this.children.forEach((child) => {
let childEl = (child instanceof Element) ? child.render() : document.createTextNode(child);
el.appendChild(childEl);
}); return el;
};

直到现在,已经可以在页面中创建一个真实的DOM结构了。

diff算法

上面已经完成了虚拟DOM -> 真实DOM的一个转换工作,现在只需要把页面上所有的改动都更新到虚拟DOM上。这就是一个diff过程。

两棵树如果完全比较时间复杂度是O(n^3),但参照《深入浅出React和Redux》一书中的介绍,React的Diff算法的时间复杂度是O(n)。要实现这么低的时间复杂度,意味着只能平层地比较两棵树的节点,放弃了深度遍历。这样做,似乎牺牲了一定的精确性来换取速度,但考虑到现实中前端页面通常也不会跨层级移动DOM元素,所以这样做是最优的。

只考虑相同等级diff,可以分为下面4中情况:

第一种。如果节点类型变了,比如下面的p标签变成了h3标签,则直接卸载旧节点装载新节点,这个过程称为REPLACE。

第二种情况。节点类型一样,仅仅是属性变化了,这一过程叫PROPS。比如

renderA: <ul>
renderB: <ul class: 'marginLeft10'>
=> [addAttribute class "marginLeft10"]

这一过程只会执行节点的更新操作,不会触发节点的卸载和装载操作。

第三种。只是文本变化了,TEXT过程。该过程只会替换文本。

第四种。节点发生了移动,增加,或者删除操作。该过程称为REOREDR。虚拟DOM Diff算法解析



如果在一些节点中间插入一个F节点,简单粗暴的做法是:卸载C,装载F,卸载D,装载C,卸载E,装载D,装载E。如下图:



这种方法显然是不高效的。

而如果给每个节点唯一的标识(key),那么就能找到正确的位置去插入新的节点。



这也就是为什么vue/react等框架,会要求我们在写循环遍历结构的时候要写key值

在vue2.0中是如何使用虚拟dom来绑定和渲染模板的

  1. 监听数据的变化

    vue内部是通过数据劫持的方式来做到数据绑定的,其中最核心的方法就是通过Object.defineProperty()的getter和setter来实现对数据劫持,达到监听数据变动的目的。

  2. vue中虚拟DOM生成DOM的过程

  3. DocumentFragment和vue异步更新队列

    DocumentFragment是容许把一些DOM操作先应用到一个dom片段里,然后再将这个片段append到DOM树里,从而来减少页面的reflow次数。

var fragment = document.createDocumentFragment();

//add DOM to fragment

for(var i = 0; i < 10; i++) {
var spanNode = document.createElement("span");
spanNode.innerHTML = "number:" + i;
fragment.appendChild(spanNode);
} //add this DOM to body
document.body.appendChild(spanNode);

vue里面的一些dom的一些变化,都是在DocumentFragment容器中去操作的,最后将这个更新片段append到el的根dom中。

走到这里,你会发现还有一个问题。就算vue使用了虚拟dom,将一些改动先同步到虚拟对象上,然后去改动真实DOM。这其中,去改动真实DOM还是使用了原生的api去操作DOM,还是会不可避免的去reflow整个页面,如果不能把这些更新操作打包起来集中去更新真实DOM,那其实完全散失了虚拟DOM的作用性,反而变得更加冗余。

这时候框架的价值就体现出来了,vue中,如果在同一次事件循环中如果观察到有多个数据变化,vue会开启一个异步更新队列,并缓冲在同一事件循环中发生的所有数据改变。然后在下一个的事件循环‘tick’中,vue刷新队列并执行实际工作。这样就可以批量的去更新多次数据变化到虚拟dom对象中,diff差异,同步到页面中的真实dom里。

在react中是如何使用虚拟dom来绑定和渲染模板的

虚拟DOM解析及其在框架里的应用的更多相关文章

  1. 原生DOM操作vs框架虚拟DOM比较

    1. 原生 DOM 操作 vs. 通过框架封装操作. 这是一个性能 vs. 可维护性的取舍.框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护.没 ...

  2. 解析vue2.0的diff算法 虚拟DOM介绍

    react虚拟dom:依据diff算法台 前端:更新状态.更新视图:所以前端页面的性能问题主要是由Dom操作引起的,解放Dom操作复杂性 刻不容缓 因为:Dom渲染慢,而JS解析编译相对非常非常非常快 ...

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

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

  4. Vue之虚拟DOM

    一.真实DOM和其解析流程? 浏览器渲染引擎工作流程都差不多,大致分为5步,创建DOM树——创建StyleRules——创建Render树——布局Layout——绘制Painting 第一步,用HTM ...

  5. React虚拟DOM浅析

    在Web开发中,需要将数据的变化实时反映到UI上,这时就需要对DOM进行操作,但是复杂或频繁的DOM操作通常是性能瓶颈产生的原因,为此,React引入了虚拟DOM(Virtual DOM)的机制. 什 ...

  6. React的虚拟DOM

    ReactJs的一大特点就是引进了虚拟dom(Virtual DOM)的概念.为什么我们需要Virtual DOM,Virtual DOM给我们带来了什么优势. 首先我们要了解一下浏览器的工作流. 当 ...

  7. JavaEE XML DOM解析

    DOM解析XML @author ixenos XML解析方式(原理) a)     DOM 解析树 b)     SAX  流事件 DOM解析对应主流工具 i.          DOM(官方) i ...

  8. React源码解析-Virtual DOM解析

    前言:最近一直在研究React,看了陈屹先生所著的深入React技术栈,以及自己使用了这么长时间.对React应该说有比较深的理解了,正好前阵子也把两本关于前端设计模式的书看完了,总感觉有一种知识错综 ...

  9. Java XML DOM解析(xPath)

    (一) XML概念 在电子计算机中,标记指计算机所能理解的信息符号,通过此种标记,计算机之间可以处理包含各种的信息比如文章等.它可以用来标记数据.定义数据类型,是一种允许用户对自己的标记语言进行定义的 ...

随机推荐

  1. PX4配置过程与踩坑

    0.前言 由于需要在GitHub下载代码,而国内访问受限,可能会出现一些问题,这里建议使用github国内镜像,参看:GitHub国内镜像网站,当然下面会给出具体解决方案. 1.步骤 1.1下载源码: ...

  2. Tomcat启动失败:java.lang.NoSuchMethodError: org.apache.tomcat.util.res.StringManager.getManager(Ljava/lang/Class;)Lorg/apache/tomcat/util/res/StringManager

    项目开发中发现服务器上Tomcat启动失败 开始定位 第一步:打开tomcat日志catalina.log: 2017-07-25 17:02:43,799 [Catalina-startStop-1 ...

  3. 使用 vim 快速对当前文件夹下的文件批量重命名

    前言 我们在使用 Linux 的时候,有很多种方法可以对文件进行重命名,例如 命令行 下的 mv 命令,或者是使用像 dolphin 这样的图形文件管理器.但是有时候需要对一个文件夹下的所有文件批量重 ...

  4. springboot 项目 运行rabbitmq(推送+消费)

    准备 先下载windos版本的mq 「rabbitmq-server-3.9.13.exe」https://www.aliyundrive.com/s/VKB63ghAJZx 点击下载 1启动rabb ...

  5. C#实现[移除文件名中的非中文字符]

    更新记录: 2022年5月28日 从程序中抽出方法复用. 处理财务文件时写的一个小函数.用于移除文件名中的非中文字符. /// <summary> /// 移除文件名中的非中文字符 /// ...

  6. 微信access_token缓存与更新

    由于Access Token有效期只有7200秒,而每天调用获取的次数只有2000次,所以需要将Access Token进行缓存来保证不触发超过最大调用次数.另外在微信公众平台中,绝大多数高级接口都需 ...

  7. php为图片添加水印

    <?php /** * 图片加水印(适用于png/jpg/gif格式) * * @author flynetcn * * @param $srcImg 原图片 * @param $waterIm ...

  8. k8s动态存储管理GlusterFS

    1. 在node上安装Gluster客户端(Heketi要求GlusterFS集群至少有三个节点) 删除master标签 kubectl taint nodes --all node-role.kub ...

  9. Error Code: 1054. Unknown column '字段名' in 'field list'

    问题描述: j博主在java开发过程中,通过读取excel中表名和字段名,动态创建insert的SQL语句,在mysql可视化工具中执行此SQL语句时,一直提示"Error Code: 10 ...

  10. C#金额数字转换中文繁体

    /// <summary> /// 数字转换中文繁体金钱 /// </summary> /// <param name="Digital">&l ...