vue核心之虚拟DOM(vdom)
浏览器画一页,要先有真实 DOM,再算样式、布局、绘制。用 JS 频繁改这些节点代价很高:每次改都可能让引擎把流程再走一遍。虚拟 DOM(vdom)用 JS 对象先在内存里描述界面,批量算出差异再一次性贴回真实 DOM。
它不是「从此不用操作 DOM」,而是把多次真实 DOM 更新收成一次;真正让 MVVM 灵活起来的,还包括跨平台和服务端渲染。Diff 把比较复杂度从 O(n³) 降到约 O(n),靠的是只做同层比较。
| Diff 类型 | 含义 |
|---|---|
| REPLACE | 节点类型变了,卸载旧节点(含子树)再装新的 |
| PROPS | 类型相同,只改属性,做更新而不是卸载 |
| TEXT | 文本节点内容变化 |
| REORDER | 子节点移动 / 增加 / 删除,有 key 才能高效定位 |
一、真实 DOM 和它的解析流程
渲染引擎工作流程大致五步:创建 DOM 树 → 创建 StyleRules → 创建 Render 树 → 布局 Layout → 绘制 Painting。
-
HTML 分析器分析元素,构建 DOM 树(标记化与树构建) -
CSS 分析器分析 CSS 文件和 inline 样式,生成样式表 -
DOM 树和样式表关联,构建 Render 树(Attachment)。每个 DOM 节点的 attach 接受样式,返回 renderer,最终连成 Render 树 -
为每个 Render 节点确定屏幕上的精确坐标 -
调用各节点 paint 方法绘制出来
DOM 树不是等文档全部加载完才开始。为了尽快上屏,不必等整个 HTML 解析完才建 Render 树和布局。Render 树也不是等 DOM 和 CSSOM 都完成才开工,三个过程会交叉:一边加载、一边解析、一边渲染。
CSS 从右往左解析(从 DOM 树自下而上通常更高效),嵌套越深越慢。
上图是 WebKit 渲染引擎工作流程。
二、JS 操作真实 DOM 的代价
传统用原生 JS 或 jQuery 改 DOM 时,浏览器往往从构建 DOM 树开始把流程再跑一遍。一次操作要更新 10 个节点,引擎收到第一个请求时不知道后面还有 9 次,于是马上执行,最终跑 10 次。第一次算完坐标,下一次更新又把坐标改掉,前面的计算作废。硬件在迭代,操作 DOM 仍然贵,频繁改就会卡。
三、为什么需要虚拟 DOM
界面由 DOM 树构成,局部变化对应某个节点变化。虚拟 DOM 就是为浏览器性能设计的:10 次更新不会立刻操作 DOM,而是把 diff 先存进一个 JS 对象,最后一次性 attach 到 DOM 树上,避免大量无谓计算。
用 JS 对象模拟节点的好处:更新先全部反映在内存里的虚拟 DOM 上,等完成后再映射成真实 DOM 交给浏览器绘制。
四、用 JS 对象实现虚拟 DOM
例如一个真实 DOM 节点:
用 JS 模拟:
Element 方法怎么实现:
第一个参数是节点名(如 div),第二个是属性(如 class),第三个是子节点(如 ul 里的 li)。对象上还会保存 key 和 count,相当于一棵虚拟 DOM 树。
有了 JS 对象,还要映射成真实 DOM:

之后所有更新都可以先反映到虚拟 DOM 上。怎么反映?Diff。两棵树完全比较是 O(n³);《深入浅出 React 和 Redux》里介绍 React Diff 是 O(n),意味着只能平层比较,放弃深度跨层。前端很少跨层移动 DOM,这样是值得的。
五、深度优先遍历,记录差异
实际会对新旧两棵树做深度遍历,每个节点有标记;遍历到一个节点就和新树对比,有差异记到一个对象里。下面用一棵新树和旧树比较。

old Tree

new Tree。平层 Diff 只有四种情况。
1、节点类型变了,例如 P 变成 H3,称为 REPLACE:卸载旧节点(含子节点)再装新节点。即使子树其实一样,为了避免 O(n³) 也值得这样做。开发上应避免运行时把 div 改成 p 这类无谓的类型变化。
2、类型一样,只是属性或属性值变了,称为 PROPS:做更新,不卸载。

查找不同属性的方法。
3、文本变了,文本也是 Text Node,直接改内容,称为 TEXT。
4、移动 / 增加 / 删除子节点,称为 REORDER。例如 A、B、C、D、E 里在 B 和 C 之间插入 F:

简单粗暴的做法是遍历新树每个节点,和旧树比,不同就卸旧装新。F 后面每个节点都会被折腾一遍:卸 C 装 F、卸 D 装 C……效率很低。

粗暴做法。如果在 JSX 里给数组或枚举加 key,就能按 key 找到位置。最小编辑距离可用 Levenshtein Distance,O(M*N);前端通常只要简单移动,把精确性换速度,降到 O(max(M,N)) 即可。

最终 Diff 出来的结果。
六、映射成真实 DOM
虚拟 DOM 有了,Diff 有了,就可以把 Diff 应用到真实 DOM:深度遍历,把差异更新进去。


会有两份虚拟 DOM(new/old)。用户操作改的是 new;old 映射成实际 DOM,经 DOM fragment 交给浏览器。new 和 old 做 diff,得到四种变换的结果表,再经 fragment 更新到页面。
vdom 的意义不只是「快一点」,更是为了跨平台、服务端渲染,以及一套还算能用的更新策略,让 MVVM 框架灵活起来。Diff 只是让比较替换更高效。DOM 最终还是要改,只不过用 fragment 把变化算完再更新一次——这就是框架的价值:业务代码不用自己把操作塞进 fragment。
链接:https://www.jianshu.com/p/af0b398602bc
一句话总结:虚拟 DOM 先在 JS 对象上记账,Diff 只做同层四种补丁,最后用 fragment 一次性打到真实 DOM。
转载请注明来源:vue核心之虚拟DOM(vdom)












