Virtual DOM

Article Source: 深入剖析: Vue 核心之虚拟 DOM

真实 DOM 解析流程 (WebKit 渲染引擎)

  1. 构建 DOM 树: 用 HTML 分析器解析 HTML 元素, 构建 DOM 树.

  2. 生成 CSSOM 样式表: 用 CSS 分析器解析 CSS 文件与元素上的 inline 样式, 生成页面的样式表.

  3. 构建 Render 树: 关联 DOM 树与样式表 (Attachment), 构建 Render 树. 调用每个节点的 attach方法, 返回 Render 对象, 用于构建此树.

  4. 确定节点坐标 (Reflow): 根据 Render 树结构, 确定每个节点的坐标与大小. (生成 Box Model)

  5. 绘制页面: 根据 Render 树与坐标, 调用节点的 paint 方法, 绘制节点.

解析顺序

  • DOM 树的构建与 HTML 加载同时进行.

  • Render 树, DOM 树, 样式表构建同时进行.

  • CSS 从右向左逆向解析, 嵌套标签越多, 解析越慢.

  • 原生 JavaScript 操作 DOM 时, 浏览器会重新构建 DOM 树. 例如需要更新 10 个节点, 浏览器收到首个请求时并不知道还有 9 次更新操作, 所以会重复执行 10 次. 操作 DOM (Repaint, Reflow) 非常消耗计算资源.

Virtual DOM 算法实现

Virtual DOM 使用 JavaScript 对象模拟 DOM. 页面更新首先将反映在此对象上, 等待更新完成后, 再将最终的对象映射成真实 DOM, 交给浏览器进行绘制.

例如需要更新 10 个节点, 虚拟 DOM 将 10 次更新的 diff 储存在 JavaScript 对象中, 随后再将该对象一次性 attach 到 DOM 树上, 避免大量无意义的计算.

JavaScript 对象模拟 DOM 树

渲染 DOM 对象

根据 tag 属性构造真实 DOM 节点并设置该节点属性, 最终通过递归构建子节点:

将构建好的节点添加到 DOM 树中:

diff 算法 - 比较两棵 Virtual DOM 树的差异

当完全比较两颗树时, diff 算法的时间复杂度为 O(n^3). 在前端开发过程中, 很少会发生跨层级的元素移动, 所以 Virtual DOM 只会比较同一层级的元素, 将复杂度降为 O(n).

diff 类型

  • Replace: 替换节点

  • Reorder: 修改子节点的顺序

  • Props: 修改节点属性 (例如添加 class)

  • Text: 修改 Text 节点的内容

使用 Depth-first Search 将所有节点与新的树中对应节点进行对比, 将差异记录到指定对象中.

列表对比算法

当子节点重新排序时, 如果按照同层级进行顺序对比, 它们都会被替换 (REPLACE) 掉. Levenshtein Distance 算法可以解决此问题. 通过 Dynamic Programming 求解, 时间复杂度为 O(M*N).

代码实现: list-diff

patch - 更新实际 DOM 树

由于 Virtual DOM 树的与实际 DOM 树结构相同, 我们可以使用 Depth-first Search 遍历 DOM 树, 并根据 Patch 的内容, 修改实际 DOM 节点.

更新指定节点

Depth-first Search 遍历 DOM 树

Vue Virtual DOM 解析

VNode 模拟 DOM 树

Vue 借鉴 snabbdom, 使用 VNode 模拟 DOM 树的节点.

  • tag: HTML 标签 (a, p, etc.)

  • data: class, style, attribute, etc.

  • children: 子节点

  • text: 文本属性

  • elm: 对应的真实 DOM 节点

  • key: 提高 diff 的效率

创建 VNode

初始化 Vue

挂载实例

mountComponent 实例化一个渲染 Watcher, 并传入一个 updateComponent 回调函数. 此回调函数调用 vm._render 方法生成 VNode 并使用 vm._update 更新 DOM.

渲染 VNode

_render 方法将实例渲染成 VNode.

_createElement 方法创建 VNode.

  • context: Context of VNode (Component)

  • tag: VNode 标签 (String 或 Component)

  • data: VNode 数据

  • children: VNode 子节点

VNode diff 算法

Vue 实例化 watcher 并将其添加到模板中所绑定的变量的依赖中. 当 model 中响应式数据发生变化, dep 数组将调用 dep.notify() 方法遍历所有依赖并更新视图 (调用 updateComponent 方法)

vm._update 方法将更新视图. vnode 参数是刚创建的 VNode.

vm.__patch__ 方法将 prevVnodevnode 进行 diff 操作, 并根据需要记录 Patch, 然后生成新的 DOM 节点来完成视图更新.

oldVnode 不存在时, 创建新的节点. 如果存在则将 oldVnodevnode 进行 diff 与 patch. patch 过程中调用 sameVnode 方法比较两个 VNode 的属性, 判断是否局部更新. 如果两个 VNode 属性相同, 则发生了、局部更新, 将两个 VNode 进行 diff. 如果两个 VNode 属性不同则跳过 diff 过程, 并创建新的真实 DOM 节点来替换旧节点.

patchVnode 方法对两个 VNode 进行 diff.

  • 对文本节点更新时, 如果文本不同则直接替换

  • 当 VNode 没有文本节点时, 开始 diff 子节点

  • 如果 oldChch 都存在且不相同, 调用 updateChildren 对子节点进行 diff

  • 如果 oldCh 不存在, 清空 oldVnode 的文本节点, 并使用 addVnodes 方法将 ch 添加到 elm (真实 DOM 节点)

  • 如果 oldCh 存在, ch 不存在, 删除 elmoldChild 子节点

  • 如果 oldVnode 有文本节点, vnode 没有, 则清空这个文本节点

子节点 diff 算法

updateChildren 方法是 diff 的最重要环节. 在开始遍历前, 首先给 oldChnewCh 分配一个 startIndexendIndex 作为遍历的索引. 当 oldChnewCh 遍历完成后 (startIndex >= endIndex), 停止 diff 过程.

patch (nodeOps)

diff 算法中, Vue 使用 nodeOps 封装的方法操作真实 DOM 结构.

Last updated