0%

通过类比 DOM 与 iOS 的思考

通过类比 DOM 与 iOS 的思考

什么是虚拟 DOM?

虚拟 DOM(Virtual DOM)是一种前端框架(如 React、Vue)中的 UI 渲染优化技术。它的本质是用 JavaScript 对象在内存中描述一棵 DOM 树,然后通过比对(diff)和最小化更新,批量把变化同步到真实 DOM。

用 iOS 和 Python 开发类比

虚拟 DOM 就像是前端的 状态快照和差异更新机制

类比 iOS UIKit

在 iOS UIKit 里,你常常需要频繁修改 UI(比如 TableView 的数据刷新)。
如果你每次数据变化都直接操作 UIKit 的每个 View,性能会变差,因为:

  • UIKit 的视图树结构复杂,频繁的 addSubview、removeFromSuperview、setNeedsLayout 等操作会导致大量不必要的重排和重绘(Layout & Rendering)。
  • 多次细粒度的 UI 操作会卡顿(比如 TableView reloadData vs. batch updates)。

虚拟 DOM 类似于这样一种机制:

  • 你先在内存里“画”好你要的 UI 状态(就像先用 Model/数据结构描述 View 层级,而不立刻操作 View)。
  • 框架帮你计算出“最小的 UI 差异(diff)”,然后一次性、高效地批量更新真实 UI 层。
  • 这样就减少了不必要的 UI 更新,提升了性能。

类比 Python 的数据变更监听

假如你用 Python 写 GUI(比如 tkinter),如果每次变量变了都立刻重画组件,性能会很差。更好的办法是:

  • 先用一个“状态快照”描述所有要变的地方(比如 dict 树结构)。
  • 最后统一“commit”,只真正重绘有变动的组件。

虚拟 DOM 的工作原理

初次渲染

  • React/Vue 会用 JS 对象结构描述真实 DOM(比如 div、ul、li),这就是虚拟 DOM 树。
  • 渲染时,把虚拟 DOM 转成真实 DOM,插入页面。

数据更新(如 setState)

  • UI 变了 → 生成新的虚拟 DOM 树(新的 JS 对象结构)。
  • 框架用“Diff 算法”对比新旧虚拟 DOM,找出哪些节点变化了。
  • 框架把最小的差异(patches)批量同步到真实 DOM。
  • 性能大幅提升,避免了无谓的操作。

更直观的类比

前端虚拟DOM iOS UIKit
JS 对象描述 DOM 结构 Model/数据结构描述 View 层级
diff 算法对比新旧树 只 diff 需要刷新的 cell 或 View
一次性 batch patch DOM UITableView 的 batch updates
减少不必要的重绘或重排 避免重复 layoutSubviews,提升流畅度

经典的虚拟 DOM 代码片段

1
2
3
4
5
6
7
8
// 这就是一个虚拟 DOM 节点
const vdom = {
type: 'div',
props: { className: 'container' },
children: [
{ type: 'span', props: { className: 'text' }, children: ['Hello, world!'] }
]
}

更新时,React 会生成新的 vdom,再跟旧的比对,最后只操作有变动的 DOM 节点。

虚拟 DOM Diff 算法原理(以 React 为例)

虚拟 DOM diff 的核心目标是高效找到两棵树的差异,并生成最小的真实 DOM 更新。

为什么不能简单对比

DOM 是树结构。两个 DOM 树之间,节点位置和层级经常变化。如果用“暴力递归比较每个节点”,复杂度是 O(n³),性能很低。

React 的优化策略

React 用了“分层递归,局部对比”策略,大大降低了复杂度(接近 O(n))。主要假设:

  • 同层节点之间是可以复用的。
  • 不同类型的节点不会互相复用。
  • 给定 key 时,只对比 key。

主要 Diff 步骤

类型不同,直接替换

如果新旧节点类型不同(如 <div> vs <span>),直接卸载旧节点、创建新节点。

类型相同,对比属性

类型一样时,对比 props(比如 class、style、事件),只 patch 变化的属性。

递归对比子节点(children)

  • 如果 children 是字符串/数字,直接替换内容。
  • 如果是数组(多个子节点),进行 key-based diff。

Keyed diff(最核心部分)

  • 给每个子节点分配唯一 key(推荐开发者传入)。
  • 先用新旧 key 映射,判断哪些节点复用、插入、删除、移动。
  • 没有 key 时,默认按索引顺序比对(效率较低,容易产生不必要的移动)。

伪代码流程:

1
2
3
4
5
6
def diff(oldNode, newNode, parentDom):
if oldNode.type != newNode.type:
replaceDom(oldNode, newNode)
else:
patchProps(oldNode.props, newNode.props)
diffChildren(oldNode.children, newNode.children)

diffChildren 的关键实现(keyed diff 简化版):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def diffChildren(oldChildren, newChildren):
# 1. 用 key 建立新旧索引映射
oldMap = {child.key: child for child in oldChildren}
newMap = {child.key: child for child in newChildren}
# 2. 找到要新增、删除、复用、移动的节点
for key in newMap:
if key in oldMap:
diff(oldMap[key], newMap[key]) # 复用+递归diff
else:
mount(newMap[key]) # 新增
for key in oldMap:
if key not in newMap:
unmount(oldMap[key]) # 删除
# 3. 根据新顺序移动 DOM
reorderDomByNewOrder()

为什么这样高效?

  • 只对最小范围内发生变化的节点做操作。
  • key 能避免大规模无意义的节点移动。
  • 没有 key 时,React 只好用“顺序 diff”,容易引发性能下降和错误。

总结

  • 虚拟 DOM 就是用 JS 对象“抽象”描述 UI 结构,和真实 DOM 解耦。
  • 通过 diff 算法,减少频繁的直接 DOM 操作(就像 UIKit 里避免频繁、分散的 View 变动)。
  • 让 UI 更新更高效、可维护、更容易跨平台(React Native 也是虚拟 DOM 的思想延伸)。