Virtual DOM
Article Source: 深入剖析: Vue 核心之虚拟 DOM
真实 DOM 解析流程 (WebKit 渲染引擎)
构建 DOM 树: 用 HTML 分析器解析 HTML 元素, 构建 DOM 树.
生成 CSSOM 样式表: 用 CSS 分析器解析 CSS 文件与元素上的
inline样式, 生成页面的样式表.构建 Render 树: 关联 DOM 树与样式表 (Attachment), 构建 Render 树. 调用每个节点的
attach方法, 返回 Render 对象, 用于构建此树.确定节点坐标 (Reflow): 根据 Render 树结构, 确定每个节点的坐标与大小. (生成 Box Model)
绘制页面: 根据 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
使用 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__ 方法将 prevVnode 与 vnode 进行 diff 操作, 并根据需要记录 Patch, 然后生成新的 DOM 节点来完成视图更新.
当 oldVnode 不存在时, 创建新的节点. 如果存在则将 oldVnode 与 vnode 进行 diff 与 patch. patch 过程中调用 sameVnode 方法比较两个 VNode 的属性, 判断是否局部更新. 如果两个 VNode 属性相同, 则发生了、局部更新, 将两个 VNode 进行 diff. 如果两个 VNode 属性不同则跳过 diff 过程, 并创建新的真实 DOM 节点来替换旧节点.
patchVnode 方法对两个 VNode 进行 diff.
对文本节点更新时, 如果文本不同则直接替换
当 VNode 没有文本节点时, 开始 diff 子节点
如果
oldCh与ch都存在且不相同, 调用updateChildren对子节点进行 diff如果
oldCh不存在, 清空oldVnode的文本节点, 并使用addVnodes方法将ch添加到elm(真实 DOM 节点)如果
oldCh存在,ch不存在, 删除elm的oldChild子节点如果
oldVnode有文本节点,vnode没有, 则清空这个文本节点
子节点 diff 算法
updateChildren 方法是 diff 的最重要环节. 在开始遍历前, 首先给 oldCh 与 newCh 分配一个 startIndex 与 endIndex 作为遍历的索引. 当 oldCh 或 newCh 遍历完成后 (startIndex >= endIndex), 停止 diff 过程.
patch (nodeOps)
在 diff 算法中, Vue 使用 nodeOps 封装的方法操作真实 DOM 结构.
Last updated