vue核心之虚拟DOM(vdom)

    |     2020年4月15日   |   vuejs, web前端技术   |     0 条评论   |    1120

浏览器画一页,要先有真实 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 树自下而上通常更高效),嵌套越深越慢。

╬в╨┼╜╪═╝_20200415113248

上图是 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 节点:

╬в╨┼╜╪═╝_20200415113316

用 JS 模拟:

╬в╨┼╜╪═╝_20200415113335

Element 方法怎么实现:

╬в╨┼╜╪═╝_20200415113552

第一个参数是节点名(如 div),第二个是属性(如 class),第三个是子节点(如 ul 里的 li)。对象上还会保存 key 和 count,相当于一棵虚拟 DOM 树。

╬в╨┼╜╪═╝_20200415113614

有了 JS 对象,还要映射成真实 DOM:

img

之后所有更新都可以先反映到虚拟 DOM 上。怎么反映?Diff。两棵树完全比较是 O(n³);《深入浅出 React 和 Redux》里介绍 React Diff 是 O(n),意味着只能平层比较,放弃深度跨层。前端很少跨层移动 DOM,这样是值得的。

五、深度优先遍历,记录差异

实际会对新旧两棵树做深度遍历,每个节点有标记;遍历到一个节点就和新树对比,有差异记到一个对象里。下面用一棵新树和旧树比较。

img

old Tree

img

new Tree。平层 Diff 只有四种情况。

1、节点类型变了,例如 P 变成 H3,称为 REPLACE:卸载旧节点(含子节点)再装新节点。即使子树其实一样,为了避免 O(n³) 也值得这样做。开发上应避免运行时把 div 改成 p 这类无谓的类型变化。

2、类型一样,只是属性或属性值变了,称为 PROPS:做更新,不卸载。

img

查找不同属性的方法。

3、文本变了,文本也是 Text Node,直接改内容,称为 TEXT。

4、移动 / 增加 / 删除子节点,称为 REORDER。例如 A、B、C、D、E 里在 B 和 C 之间插入 F:

img

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

img

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

img

最终 Diff 出来的结果。

六、映射成真实 DOM

虚拟 DOM 有了,Diff 有了,就可以把 Diff 应用到真实 DOM:深度遍历,把差异更新进去。

img

img

会有两份虚拟 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)
本文链接地址:https://ai.zhousir.top/?p=2703
回复 取消