网上关于 Virtual DOM(下称VDOM) 和 直接操作 DOM 来更新页面谁的性能更高有许多争论。这里就 VDOM 的性能问题简单谈下自己的理解。
一、什么是 VDOM
VDOM 本质上是一个 Javascript 对象,用来描述 DOM 结构,如:
<html>
<head></head>
<body>
<ul class="list-ul">
<li class="list-one">List One</li>
</ul>
</body>
</html>
可以用如下对象表示:
const vdom = {
tagName: "html",
children: [
{ tagName: "head" },
{
tagName: "body",
children: [
{
tagName: "ul",
attributes: { "class": "list-ul" },
children: [
{
tagName: "li",
attributes: { "class": "list-one" },
textContent: "List One"
}
]
}
]
}
]
}
在实际的生产环境需要将这个 JS 对象转化成真实的 DOM 元素。
二、Js 操作 DOM 的开销
1、浏览器渲染引擎工作流程大致分为如下步骤:
-
解析 HTML 创建 DOM 树
当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。 - 解析 CSS 创建 CSS 规则树
- 合并 DOM 树和 CSS规则,生成 render 树
- Layout / Reflow:布局 render 树,计算各个元素的尺寸和位置等
- Paint:绘制页面内容
2、JS 频繁操 DOM 的开销:
- 在上述渲染过程中,前3点可能要多次执行,比如 Js 操作 DOM、更改 CSS 样式时,浏览器又要重新构建 DOM、CSS 规则树,重新 render,重新 Layout、Paint;
- Layout 在 Paint 之前,因此每次 Layout 重新布局(Reflow 回流)后都要重新 Paint / 渲染,这时又要去消耗 GPU 资源
- Paint 不一定会触发 Layout,比如颜色、背景的改变,只需要 Repaint / 重绘
- 图片下载完也会重新出发 Layout 和 Paint
- 虽然浏览器针对渲染流程有优化,但是这个过程开销还是巨大的。
三、为什么需要 VDOM
假如在实际生产环境中,有这么一个列表:
<ul>
<li><span>item 1</span></li>
<li><span>item 2</span></li>
<li><span>item 3</span></li>
...
<li><span>item 100</span></li>
</ul>
我们现在需要更新列表的,从后端拿到了数据,但是新的数据只有第50行的数据有变化。
1、最简单最节约心智的办法是,我们不关心新数据与老数据之间的差异,直接 innerHTML 更新整个 ul 标签里的内容。但是这样就造成了不必要的 DOM 操作开销,毕竟只需要更新一个节点,但是为了省事,99次操作是浪费资源的无用功。
2、或者我们逐个对比数据,发现是第50行有变化,直接将第50行的 span 用 innerText 更新内容即可。
3、虽然手动更新第50行内容达到了最小操作,但是每次从后端拿到新数据,我们并不能都知道是哪些行数据有变化,这个时候就需要写一个通用的方法来比较新旧数据的变化,并只去更新数据有变化的节点。
但是这样还不够,这个通用方法也只是满足了这一个列表的数据对比和节点更新,如果我们的项目中还有几十、几百个其它的列表呢?
这个时候 VDOM 就派上用场了。我们把整个页面抽象成 Js 对象(VDOM),每次的更新数据,我们都先更新 VDOM,再通过比较 新旧 VDOM 的变化,找到具体要更新的节点,再去操作具体的 DOM。
这个时候可能有人要问了,那在更新 VDOM 的时候不也做了很多无用功的操作吗?
对,但是 VDOM 的操作都是纯 Js 的计算,大家要明确的一点是 Js 计算(特别是在 V8 引擎的加持下)要比真实 DOM 操作开销小得多,最重要的是再也不用操心到底哪些数据有更新了,直接无脑用 VDOM 就是,开发效率提高了!
四、那到底谁的效率更高
用这个列表举例:
<ul>
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
...
<li>item 100</li>
</ul>
1、当列表新数据更新了第50、60行时:
- 手动对第50、60行的 span 节点进行 innerText操作,这是性能最高的,我们计它的耗时为
。
- 直接 innerHTML 整个 ul 列表,不考虑浏览器渲染优化等情况,简单的认为会多出88次节点操作,计为
。
- 通过 VDOM 来更新 DOM :
抽象 DOM 结构为 VDOM -> Diff 策略比较变化 -> 更新真实 DOM
则耗时为 Js 计算 VDOM 变化()与最小节点操作(
)之和,计为:
+
。
前面也说了得益于现代浏览器的高效,Js 计算是非常快的,故 +
<<
。
综合比较三种更新方式, <
+
<<
。
可以看出手动去进行最小节点操作是性能最好的,但是其心智负担也不小。
2、当 ul 中所有数据都变了,那就能直接无脑 innerHTML 进行更新,因为此时更新所有节点就是最小节点操作。
所以一个项目能确定基本上每一页的内容都不相同,几乎要全部更新,那可以不用 VDOM,直接用 innerHTML 即可。
反之一个项目,有很多列表,有众多增删改查操作,那用 VDOM 是十分有意义的。
所以抛开场景谈性能就如同抛开剂量谈毒性,都是耍流氓。用 VDOM 更新真实 DOM 其实是在节约开销(运行效率)和节约心智(省事、提高开发效率)之间找到一个比较好的平衡点。不然为什么人家 React 和 Vue 要用 VDOM 呢?成千上万人验证过的东西,能流行一定有它的道理