SwiftUI 介绍文档的第一行总会提到“声明式(declarative)框架”。React 和 Jetpack Compose 也都称自己为声明式。
但当有人问“什么是声明式?”时,往往很难回答。“就是代码很整洁”并不是正确答案。
声明式和命令式的区别可以用一句话概括。你写的是“如何(How)”,还是“什么(What)”?
本文通过日常例子理解这一标准,再用 UIKit 和 SwiftUI 代码进行验证,并讨论声明式并非没有代价。
以下是核心总结。
- 命令式:逐步指示到达目标结果的过程 — How
- 声明式:描述想要的结果,把过程交给系统 — What
- SQL、HTML、map/filter 和 SwiftUI 都是声明式的代表案例
- 声明式 UI 的本质:声明“状态是这样时,界面就是这样”,状态变化后的更新由框架处理
用出租车来比喻
命令式就像直接给司机指路:“右转,直行 300 米,在红绿灯处左转……”你逐一指示每个步骤,而这些步骤共同产生抵达目的地的结果。
声明式则是说:“请带我去江南站。”只说明目的地(What),路线选择(How)交给司机和导航。
两者都能到达目的地。区别在于是由我拥有过程,还是只拥有结果描述。
在代码中,同一件事可以用两种方式编写
下面分别用两种方式编写筛选偶数并求平方的代码。
// 命令式:我指定如何遍历以及把结果放在哪里
var result: [Int] = []
for n in numbers {
if n % 2 == 0 {
result.append(n * n)
}
}
// 声明式:只描述我想要什么
let result = numbers.filter { $0 % 2 == 0 }.map { $0 * $0 }
命令式版本会暴露循环变量、中间数组、顺序等“过程部件”。声明式版本只保留“筛选偶数并求平方”这一意图。遍历方式隐藏在 filter 和 map 内部。
你可能已经在使用更熟悉的声明式形式。SQL 只写“给我符合这些条件的行”,不写索引搜索方式;HTML 只声明结构——“这里是标题,这里是段落”——不写渲染过程。
到了 UI 中,差异会显著扩大
命令式 UI(UIKit)会编写改变界面的过程。
// UIKit: 每次状态变化时直接指示界面操作
func updateBadge(count: Int) {
if count > 0 {
badgeLabel.isHidden = false
badgeLabel.text = "\(count)"
} else {
badgeLabel.isHidden = true
}
}
这种方式的难点在于,开发者必须持续跟踪“当前界面处于什么状态”。如果更新路径很多,就很容易漏掉一条,导致“数据变了但界面没变”的典型 bug。
声明式 UI(SwiftUI)声明特定状态下界面应有的样子。
// SwiftUI: count声明状态是这样时,界面就是这样
struct BadgeView: View {
let count: Int
var body: some View {
if count > 0 {
Text("\(count)").badgeStyle()
}
}
}
count 变化时,不需要编写如何修改界面。SwiftUI 会比较之前的声明和新的声明,只更新必要的部分。UI 就变成了“状态的函数”。由于更新过程的所有权交给了框架,也就没有会遗漏的更新代码。
声明式并非没有代价
为了保持平衡,也需要看看另一面。
隐藏过程的代价是,需要过程时会让人无从下手。像“将滚动精确移动到这个偏移量”这样的命令式需求,在声明式框架中反而需要绕行处理。
调试的方式也不同。命令式可以沿着过程追踪,而声明式需要追查框架的判断,例如“为什么又重绘了?”。隐藏的 How 并不是完全可以不了解。
性能调优最终仍要回到对内部机制的理解。当 SwiftUI 过度重新计算视图时,必须了解 diffing 和依赖跟踪的运行方式才能解决问题。
因此,准确的观点不是“声明式更优越”,而是对抽象层级的选择。交出过程的所有权后,代码会围绕意图变得简洁,但细粒度控制和对内部机制的理解会成为新的课题。
总结
- 命令式指示到达结果的过程(How),声明式描述想要的结果(What)
- SQL、HTML、map/filter 都是已经熟悉的声明式形式
- 命令式 UI 编写改变界面的过程,容易出现状态与界面不一致的 bug
- 声明式 UI 声明“状态是这样时,界面就是这样”,更新由框架负责(UI = 状态的函数)
- 声明式的代价:难以进行细粒度的过程控制,最终仍需理解框架的内部运行机制
- 本质不是优劣,而是选择由谁拥有过程

