5.1 声明式范式:描述树而非修剪树


5.1 声明式范式:描述树而非修剪树

本节摘要:前四章用命令式伺候界面——造视图、改属性、手动刷新。SwiftUI 换世界观:你只声明"给定这份数据,界面长什么样",状态变化后的同步全部交给框架。本节用同一个浇水需求写出两种范式的完整对照,拆解声明式的三步机制(状态、描述、差量更新),并诚实列出声明式欠的账——它不是免费的。

同一个需求,两种世界观

需求很简单:点一下"浇水"按钮,倒计时标签从"还剩 2 天"变成"已浇水"。第 3 章的命令式写法你已经很熟了——每个变化都要亲手指挥:

// 命令式:UIKit——我指挥每一步 final class WaterVC: UIViewController { private let label = UILabel() private let button = UIButton(type: .system) private var passedDays = 2 // 状态藏在控制器属性里 private let interval = 3 @objc private func waterTapped() { passedDays = 0 // 第一步:改状态 label.text = "已浇水" // 第二步:亲自改标签 label.textColor = .systemGreen // 第三步:亲自改颜色 button.isEnabled = false // 第四步:亲自禁用按钮 button.setTitle("已浇水", for: .normal) // 第五步:亲自改按钮标题 } // 隐藏成本:明天还要加"环变绿、副标题更新"——回到这里继续加第六步第七步 }

SwiftUI 版:

// 声明式:SwiftUI——我只描述"状态对应的样子" import SwiftUI struct WaterView: View { @State private var passedDays = 2 // 状态:唯一的真相源 let interval = 3 var body: some View { // body 是"状态 → 界面"的函数 VStack(spacing: 16) { Text(passedDays >= interval ? "该浇水了" : passedDays == 0 ? "已浇水" : "还剩 \(interval - passedDays) 天") // 文案:现在时态描述 .foregroundColor(passedDays == 0 ? .green : .primary) Button(passedDays == 0 ? "已浇水" : "浇水") { passedDays = 0 // 唯一要做的事:改状态 } .disabled(passedDays == 0) // 禁用条件也描述出来 } } } // 点击后:passedDays 变 0 → 框架重算 body → 与旧树求差 → 只更新变化的那几处 // 界面效果与命令式版完全一致,代码里没有任何一步"亲自改界面"

数一数:命令式里每新增一个界面反应就多一行指挥代码;声明式里界面反应写在描述里,状态一变自动生效。改界面的工作没有消失,只是从你的代码搬进了框架

图 5-1 命令式流水线与声明式回路

图 5-1 命令式流水线与声明式回路

三步机制拆解:状态、描述、差量

声明式的一切神秘都能拆回三步。第一步状态@State 标记的属性是视图的唯一真相源——这个标签为什么显示"已浇水"?因为 passedDays 是零,没有第二个答案。第二步描述:body 是个计算属性(2.4 的概念),每次访问都按当前状态现算出"界面应该长什么样"的描述树——它不是界面本身,是图纸。第三步差量:框架拿新图纸与旧图纸对比,只把变化的节点同步到屏幕。

用一段代码把"body 是函数"这件事看穿:

struct CounterView: View { @State private var count = 0 var body: some View { VStack { Text("浇了 \(count) 次") Button("记一次") { count += 1 } } } } // 每次点按钮: // count 变化 → 框架重新求值 body → 得到新的 Text("浇了 1 次") → 差量替换那一处文字 // 控制台验证(在按钮动作里加 print): // 点击前 count = 0 // 点击后 count = 1 界面文字同步变成"浇了 1 次"

对照第 3 章的思维:UIKit 里"界面是名词"(造好摆在那,逐步修);SwiftUI 里"界面是动词"(状态到图纸的映射,随叫随算)。第 1 章那十行 UIKit 与十行 SwiftUI 的对比,此刻应该真正看懂了。

案例:从"忘了刷新"到"不可能忘"

背景:第 4 章反复强调"改了模型必须 reload",团队真实事故是改了数据忘刷表格。操作:把植物列表搬到 SwiftUI(完整版在 5.5),先看行视图的状态驱动版。

struct PlantRow: View { let plant: Plant // 数据进门(值语义,2.4 的红利) var body: some View { HStack { Circle().fill(plant.shouldWater ? Color.red : Color.green).frame(width: 10, height: 10) Text(plant.name) Spacer() // 撑开,把后面的文字推到右边 Text(plant.statusText).foregroundColor(.secondary) } } } // 使用侧一旦 plant 被新值替换,整行自动按新数据重画——不存在"忘了刷新"这个 bug 类别

解读:值语义在这里兑出现金——Plant 是结构体,替换即"新数据",框架比对新旧值决定重画,引用类型就做不到这么干净(2.5 讲过身份共享的陷阱)。变式:若行内还要"点按钮改这株植物",状态提升与绑定是 5.3 的主题;本节只要接受一个设定:描述树是状态的影子,影子永远跟着本体

💡 关键直觉:写 SwiftUI 时问自己"这个界面是哪份数据的函数"。答得出来,架构就对了;答不出来,状态多半放错了层。

本节要点回顾

  • 范式之别:命令式亲自指挥每步刷新,声明式只改状态、同步交给框架;
  • 三步机制:状态唯一真相源、body 是状态到图纸的函数、差量更新只动变化处;
  • 代码形态:界面反应写成描述的一部分,新增反应不再新增指挥代码;
  • 欠的账:像素级编排绕路、排错直觉要重建、部分场景仍需 UIKit 兜底;
  • 值语义红利:结构体模型的替换即差异,天然适配差量更新。

图纸由谁执笔、怎么读——下一节拆 View 协议与修饰符链。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U