【总结】理解虚拟DOM与真实DOM
一、真实DOM
1、WebKit的WebCore层包含了HTML引擎,负责将HTML文档解析成DOM树,由标签节点构成的文档树。最终解析出来的DOM树交由DOM模块负责管理,这个DOM就是我们俗称的真实DOM。
2、JavaScript Core是webKit默认的javascript引擎(现在被替换为了V8引擎),负责执行javascript代码。
3、为了给JavaScript提供操作DOM树的能力,浏览器在全局对象window上为JavaScript封装了一个document对象,然后在该对象上提供了大量的DOM操作接口。以及jq的操作$()DOM的方法。
webkit内核
4、真实DOM查询节点的几种方法
getElementById()
getElementsByClassName()
getElementsByName()
getElementsByTagName()
getElementsByTagNameNS()
document.querySelector()
ParentNode.querySelector()
document.querySelectorAll()
ParentNode.querySelectorAll()
在浏览器控制台查看document.getElementById,可以看到它的函数体是{ [native code] },这表示它是一个用C++编写的函数,因此无法查看具体实现。当我们在调用这个函数时,JavaScript引擎并没有直接与DOM模块交互,而是由浏览器来操作DOM模块,随后再把操作结果返回给JavaScript引擎。这种借助父级模块实现两个同级模块交互的通信方式非常常见。
正是由于JavaScript需要借助浏览器提供的DOM接口才能操作真实DOM,所以操作真实DOM的代价往往是比较大的(这其中还涉及C++与JavaScript数据结构的转换问题)。再加上修改DOM经常导致页面重绘,所以一般来说,DOM操作越多,网页的性能就越差。
截止到目前,如何有效地减少对真实DOM的操作,仍然是前端性能优化的一个关键点。虚拟DOM就是目前较为流行的一个解决方案。
二、虚拟DOM(保存在JS的内存中)
JavaScript无法直接操作DOM是带来上述性能问题的根源之一(其他原因包括,真实DOM树的体积非常庞大,而且操作它会导致页面重绘)。那么能不能在JavaScript内存中,以js对象的形式也描述一棵DOM树呢?
Vue采用了响应式系统和虚拟DOM结合的方式。
对于开发者而言,虚拟DOM的实现是透明的,它只是框架自动高效更新DOM的一种内部解决方案。开发者需要按照框架给定的语法定义数据和视图的绑定关系,随后就只需要关心数据变化(即业务逻辑)即可。
虚拟DOM本质上是一个js对象,通过对象来表示真实的DOM结构。tag用来描述标签,props用来描述属性,children用来表示嵌套的层级关系。
const vnode = {
tag: 'div',
props: {
id: 'container',
},
children: [{
tag: 'div',
props: {
class: 'content',
},
text: 'This is a container'
}]
}
//对应的真实DOM结构
<div id="container">
<div class="content">
This is a container
</div>
</div>
虚拟DOM的更新不会立即操作DOM,而是会通过diff算法,找出需要更新的节点,按需更新,并将更新的内容保存为一个js对象,更新完成后再挂载到真实dom上,实现真实的dom更新。
通过虚拟DOM,解决了操作真实DOM的三个问题。
无差别频繁更新导致DOM频繁更新,造成性能问题
频繁回流与重绘
开发体验
另外由于虚拟DOM保存的是js对象,天然的具有跨平台的能力,而不仅仅局限于浏览器。
虚拟DOM实现原理主要分三部分:
通过js建立节点描述对象
diff算法比较分析新旧两个虚拟DOM差异
将差异patch到真实dom上实现更新
三、虚拟DOM与真实DOM的区别(dom操作量越大越明显)
虚拟DOM不会进行排版与重绘操作;
虚拟DOM进行频繁修改,然后一次性比较并修改真实DOM中需要改的部分,最后并在真实DOM中进行排版与重绘,减少过多DOM节点排版与重绘损耗;
真实DOM频繁排版与重绘的效率是相当低的;
虚拟DOM有效降低大面积(真实DOM节点)的重绘与排版,因为最终与真实DOM比较差异,可以只渲染局部;
虚拟DOM的损耗计算:
// bad
let ul = document.querySelector('#mylist');
ul.append(li)
ul.append(li)
ul.append(li)
// good,两次重排重绘
let ul = document.querySelector('#mylist');
ul.style.display = 'none'; // 减少重绘
ul.append(li)
ul.append(li)
ul.append(li)
// better,一次重排重绘
let fragment = document.createDocumentFragment();
fragment .append(li)
fragment .append(li)
fragment .append(li)
ul.appendChild(fragment);
四、Diff算法
为了避免不必要的渲染,按需更新,虚拟DOM会采用Diff算法进行虚拟DOM节点比较,比较节点差异,从而确定需要更新的节点,再进行渲染。vue采用的是深度优先,同层比较的策略。
新节点与旧节点的比较主要是围绕三件事来达到渲染目的
创建新节点
删除废节点
更新已有节点
如何比较新旧节点是否一致呢?
vue-diff流程图
当数据发生改变时,set方法会让调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视图。
具体参考vue源码
总结:
要理解为什么需要虚拟DOM,必须弄清楚JavaScript引擎和DOM模块之间的关系,并体会由这种关系导致的DOM操作的性能问题。虚拟DOM设计的核心就是用高效的js操作,来减少低性能的DOM操作,以此来提升网页性能。
从一定程度上来说,是浏览器的架构问题催生了虚拟DOM,而这个架构问题几乎需要重构浏览器内核才能解决,所以目前虚拟DOM仍广为流行。但是,框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能,性能比较也要区分场景,diff不是免费的。
参考:https://www.cnblogs.com/wind-lanyan/p/9061684.html