虚拟DOM作用:以js的形式在内存中描述真实的DOM结构,这样当页面内容发生变动时,React可以通过对比前后DOM, 计算出如何以最小的代价操作真实DOM.
1、真实DOM操作的性能问题
- jQuery直接操作真实DOM
1 | <body> |
在本例中, jQuery
只是提供了一层对原生 DOM
操作的封装。我们可以简单手写一个 $
实现:
通过这个简单的封装我们可以发现, jQuery
的主要工作是提供对 原生DOM
操作的统一实现(另外还有对 原生js
的增强,如 ajax
、动画等)。换句话说,使用 jQuery
,本质上还是在直接操作 真实DOM
。
那么直接操作
真实DOM
有什么问题呢?
这就要从浏览器内核的构成说起了。我们以 webkit
内核为例,它大致包含以下模块:
先来看左侧的深色区域,它是 webkit
内核的 WebCore
层。该区域左下角的 HTML模块
代表 HTML引擎
,作用是解析 HTML文档
。当浏览器下载了一个 HTML文档
后,它负责将该文档解析成 DOM树
,也就是由一个个标签节点构成的文档树。最终解析出来的 DOM树
将交由它右侧的 DOM模块
负责管理,而这个 DOM树
就是我们平常所说的 真实DOM
。在深色区域的右侧紧邻着的是 JavaScriptCore
,即 webkit
默认的 JavaScript引擎
(在 Chrome
和 chromium
中它被替换为了 V8
),它负责执行 JavaScript
代码。
我们知道, JavaScript
具备 操作DOM树
的能力。但是从浏览器内核的结构可以看到, DOM树
由 DOM模块
负责管理,在浏览器内核中单独占有一块内存,而这块内存与 JavaScript引擎
所管理的内存并无直接关系。换句话说, JavaScript引擎
不能直接操作 真实DOM树
。为了给 JavaScript
提供 操作DOM树
的能力,浏览器在全局对象 window
上为 JavaScript
封装了一个 document
对象,然后在该对象上提供了大量的 DOM操作接口
,这些接口都是用 C++
实现的。如:
在浏览器控制台查看 document.getElementById
,可以看到它的函数体是 { [native code] }
,这表示它是一个用C++编写的函数,因此无法查看具体实现。当我们在调用这个函数时,JavaScript引擎并没有直接与DOM模块交互,而是由浏览器来操作DOM模块,随后再把操作结果返回给JavaScript引擎。这种借助父级模块实现两个同级模块交互的通信方式非常常见。
正是由于JavaScript需要借助浏览器提供的DOM接口才能操作真实DOM,所以操作真实DOM的代价往往是比较大的(这其中还涉及C++与JavaScript数据结构的转换问题)。再加上修改DOM经常导致页面重绘,所以一般来说,DOM操作越多,网页的性能就越差。
我们以一个简单的图例来理解这个过程:
有人可能会问,既然DOM操作的代价如此之大,为什么不由JavaScript引擎来管理DOM呢?不要忘了,世界上第一款浏览器诞生于1990年,那时候就已经出现了DOM的原始概念,而JavaScript则直到1995年才诞生。换句话说,DOM的出现早于JavaScript。因此,如果要由JavaScript来管理DOM,那就意味着浏览器内核必须重构。而让浏览器开发者为了一个早期被称为“玩具语言”的JavaScript重构浏览器内核显然是不可能的。不过随着JavaScript的发展,由JavaScript引擎直接管理DOM的构想已经被提上了chromium的计划列表(目前只是定为长期计划,要真正实现还需要相当长的时间)。
所以,截止到目前,如何有效地减少对真实DOM的操作,仍然是前端性能优化的一个关键点。虚拟DOM就是目前较为流行的一个解决方案。
2、虚拟DOM的作用
显然,JavaScript无法直接操作DOM是带来上述性能问题的根源之一(其他原因包括,真实DOM树的体积非常庞大,而且操作它会导致页面重绘)。那么能不能在JavaScript内存中,以js对象的形式也描述一棵DOM树呢?当然可以,这就是我们所说的虚拟DOM树。
可以看到,虚拟DOM并不能消除原生的DOM操作,你仍然需要通过浏览器提供的DOM接口来操作真实DOM树,才能使页面发生改变。虚拟DOM的设计似乎是多此一举。
但是虚拟DOM带来了一个重要的优势,那就是我们可以在完全不访问真实DOM的情况下,掌握DOM的结构,这为框架自动优化DOM操作提供了可能。举例来说,如果我们本打算手动进行三次真实DOM操作,而框架在分析了虚拟DOM的结构后,把这三次DOM操作简化成了一次,这不就带来了性能上的提升吗?再甚者,如果连这一次DOM操作都可以由框架自动完成(自动更新的前提是我们要定义视图和数据的绑定关系),而我们只需要负责处理数据,那么虚拟DOM的价值体现得就更明显了。
React
就是这么使用 虚拟DOM
的
当我们使用jsx语法定义一个模板时,React会用它生成一个由JavaScript描述的虚拟DOM树,并将其保存在JavaScript内存中。这个虚拟DOM树还保留了我们在模板中定义的数据和视图的绑定关系,这为React自动根据数据变化更新视图提供了可能。随后当数据发生变化时,React重新生成一个虚拟DOM树,通过对比两个虚拟DOM树的差异,React就可以知道该如何高效地更新视图。接着它就会调用原生的DOM接口,去更新真实DOM。过程大致如下:
对于开发者而言,虚拟DOM的实现是透明的,它只是框架自动高效更新DOM的一种内部解决方案。开发者需要按照框架给定的语法定义数据和视图的绑定关系,随后就只需要关心数据变化(即业务逻辑)即可。
当然了,虚拟DOM并不是解决DOM操作性能问题的唯一解决方案,Vue的响应式系统也是一种重要的解决方案。从某种程度上来说,Vue依靠响应式系统可以实现“精确定点更新”,即直接定位到哪个DOM节点需要更新,而不需要经过虚拟DOM的比对,不过“精确定点更新”的内存代价偏大,因此目前Vue采用了响应式系统和虚拟DOM结合的方式,本文暂不详述。
最后我们来看一下Vue中虚拟DOM树的结构,实际上它就是一个js对象而已,我们以下面的模板为例:
<template><div id="app"> <ul> <li v-for=“item in items”> itemid: {{ item.id }} </li> </ul> </div></template>
对应的虚拟DOM:
总结
要理解为什么需要虚拟DOM,必须弄清楚JavaScript引擎和DOM模块之间的关系,并体会由这种关系导致的DOM操作的性能问题。虚拟DOM设计的核心就是用高效的js操作,来减少低性能的DOM操作,以此来提升网页性能。
从一定程度上来说,是浏览器的架构问题催生了虚拟DOM,而这个架构问题几乎需要重构浏览器内核才能解决,所以目前虚拟DOM仍广为流行。如果未来的某一天,真实DOM被迁移到JavaScript内存中,虚拟DOM的价值实际上也就不存在了。
本文转载至:
作者:夕山雨
地址:https://baijiahao.baidu.com/s?id=1676526364115609709&wfr=spider&for=pc