本节摘要:声明式 UI 的核心是"你告诉系统想要什么,而不是如何去做"——界面是状态的函数,状态一变界面自动重建。本节对比命令式与声明式两种范式,讲清 State 驱动 UI 更新的完整流程,以及 React Native 与 Flutter 如何用同一思想实现高效的界面更新。这是理解现代 UI 开发的思维总纲。
阅读完本节,你应当能够:
先做一个思维实验。想象你在点一份萨拉米披萨。命令式点法是这样:你盯着后厨的每一步——拿面粉、加水、揉面、发酵、擀平、涂酱、撒芝士、放萨拉米、进烤箱、烤二十分钟、切片、装盒。任何一步做错,披萨就毁了。声明式点法只有一句:"给我一份萨拉米披萨"。至于后厨怎么做出它,你不用操心,结果保证是你要的。
旧式 UI 开发就是命令式点法。你用 jQuery 操作 DOM 时,代码长这样:先找到元素、改它的颜色、往父节点追加一个子节点、监听点击再手动改回颜色。状态一变,你得自己记得"该改哪些地方、怎么改"。
声明式 UI 是披萨式点法。你声明"界面在某个状态下应该长什么样",框架负责把它变成现实。状态变了,你只更新状态,界面怎么变是框架的事。
一句话点题:命令式问"怎么改界面",声明式问"界面该是什么样"。
声明式 UI 最核心的命题是:UI = f(State)。界面是状态的函数。给定相同的状态,界面呈现永远相同;状态变了,函数重新求值,界面跟着变。
这个命题带来一个巨大的好处:可预测性。调试时你只需要关注状态对不对,界面表现是确定的,不会再出现"同样的数据、不同的界面"这种玄学问题。测试也变简单了——给定一组状态,断言界面应该是什么样。
| 对比维度 | 命令式 | 声明式 |
|---|---|---|
| 关注点 | 如何操作界面 | 界面该是什么样 |
| 代码形态 | 查找、修改、增删元素 | 声明状态与界面的映射 |
| 状态变化 | 手动同步到界面 | 自动触发重建 |
| 更新方式 | 逐条指令 | 差异比较后最小更新 |
| 可预测性 | 受更新顺序影响 | 同状态必同界面 |
| 典型代表 | jQuery、原生视图操作 | React Native、Flutter |
命令式的痛点在状态复杂时集中爆发:手动管理 UI 更新的逻辑繁琐易错、随着应用增长难以追踪、频繁的 DOM 或视图操作引发性能问题、相似更新逻辑在多个地方重复。声明式把这些都交给了框架:你只管状态,框架管界面。
光说"繁琐易错"不够直观,我们把命令式写法的样子摆出来看。假设页面上有一个购物车数字,用户每加一件商品要更新它。命令式大概是这样:
// 命令式:找到元素,改内容,还得记住改哪 const cartCount = document.getElementById('cart-count'); cartCount.textContent = String(totalItems);
代码本身不难,难的是维护:当页面上还有价格、运费、折扣、库存这几个数字都要跟着变时,你得在每一个操作点手动更新所有相关元素。今天加一个"满减"逻辑,就得在所有操作点再补一处更新。改一处漏一处,是最常见的线上事故来源。
声明式写法把这个负担反转了:你维护的只有一个"购物车状态",页面上的数字全部由状态推导。状态一变,所有依赖它的界面自动跟着变,不存在"漏改"的可能。这个"由状态统一推导"的思想,就是声明式可维护性的来源。
还有一个常被忽视的收益:声明式让界面代码可以被审查和测试。命令式代码里,"界面更新"散落在事件处理、回调、异步逻辑各处,看代码的人很难追踪"这个界面在什么情况下会变成什么样"。声明式组件把"给定状态 → 输出界面"封装成一个确定函数,测试可以直接调用它、传入不同状态断言输出。这解释了为什么声明式框架通常测试基础设施更完善——不是因为框架厉害,而是因为范式本身就为测试铺好了路。
以 React Native 为例,从状态变化到界面更新走完六步:

这一链路里最值得记住的是第 5、6 步:框架不是"整页重画",而是"找差异、改差异"。这是声明式性能不差的关键,也是 RN 与 Flutter 都选择的路。
RN 用虚拟 DOM:组件渲染产出虚拟 DOM,状态变化后生成新虚拟 DOM,与旧树做 diff,计算出最小变更再应用到原生视图。Flutter 用 Widget 树协调:重建 Widget 树,与上一棵树比较,只重建变化的 Element。名字不同、机制相近——都是"描述界面、比较差异、最小更新"。
习惯一:想清楚状态,而不是想清楚操作。写界面前先问"这个界面依赖哪些状态",状态列全了,界面自然清晰。
习惯二:状态放在它该在的地方。组件自己的状态放组件内部,跨组件共享的状态考虑全局管理(第 5 章展开)。状态位置错了,界面就会难维护。
习惯三:别直接改数据,产生新数据。RN 与 Flutter 都靠引用比较判断变化,原地改对象不产生新引用就不会触发更新。这是新手最容易踩的坑。
⚠️ 常见坑:在声明式代码里偷偷写命令式操作——直接操纵某个控件的实例去改它的属性。这违背声明式原则,会造成状态与界面不同步、更新混乱。正确的做法永远是"改状态,让框架去更新界面"。
💡 关键直觉:判断一段 UI 代码是否声明式,看它有没有"直接操作界面对象"。如果代码里出现"找到某元素、设置某属性"这类指令,就是命令式的;如果只是声明状态与界面的映射,就是声明式的。
声明式不是万能钥匙。极少数场景下命令式更直观:高频逐帧动画的精细控制、需要直接操作图形上下文的自定义绘制、与第三方原生视图的深度交互。Flutter 的 AnimationController、自定义 CustomPainter 都属于这类。正确姿势是:主体用声明式,例外才用命令式,而不是反其道而行。
问:声明式会不会比命令式性能差? 不会,前提是框架的差异算法在正常工作。声明式把"全量描述"与"最小更新"组合起来:描述时是全量的,更新时只动差异。相比之下,命令式"精确操作"反而容易因漏更新或多更新而出问题。真正影响性能的从来不是范式,而是对差异算法的滥用——比如每次渲染都生成新的匿名对象导致无法复用。
问:理解了声明式,对写业务代码有什么直接帮助? 直接帮助在于"设计思路":遇到界面需求,先想状态结构,再想界面如何由状态推导,而不是先想"我要操作哪些控件"。这个思路一换,组件拆分、状态提升、可复用性这些设计问题都会迎刃而解。可以说,声明式思维是后续所有组件与状态相关章节的思想地基。
问:RN 的虚拟 DOM 和 Flutter 的 Widget 树比较,哪个更快? 没有简单的胜负。两者都采用"描述、比较、最小更新"的策略,性能表现取决于具体场景与实现细节。对业务开发而言,这个差异远小于"是否合理组织状态与组件"的影响。新手完全不需要为这两者的性能差异纠结——先把声明式思维用对,比什么都重要。
声明式不仅改变了写法,更改变了你设计状态的方式。命令式时代你会想"界面需要哪些操作",声明式时代你会想"界面需要哪些状态"。这个转变带来一个实际好处:状态设计先行,界面自然跟随。设计新页面时先列状态清单(字段、类型、默认值),再写界面,顺序反过来往往更容易返工。这也是为什么状态管理(第 5 章)在声明式框架里如此重要——状态是界面的输入,输入设计得好,输出才稳定。
把一段命令式代码翻译成声明式,是很好的训练。拿"切换开关控制文本颜色"举例:命令式写法是"点击时把文本颜色改成红色/蓝色";声明式写法是"isOn 为真时文本红色,否则蓝色"。注意声明式里没有任何"改颜色"的操作,只有"状态到界面"的映射。多做几组这类翻译练习,声明式的"描述而非操作"就会成为本能。这个练习也可以在第 4 章做组件时顺带完成。
声明式的思维内核已经建立,第 4 章我们开始真正用组件搭界面——把"描述界面"落到具体的一行行组件代码上。