UI编程的发展史 : 结合命令式UI和声明式UI

UI编程的发展史 : 结合命令式UI和声明式UI

聊到UI编程的发展,就好比开车从"手动挡"进化到"自动挡"。从80年代个人计算机刚兴起时的"步步指令",到如今用几行代码就能搞定的声明式开发,这几十年的变化,简直是把开发者从繁琐的重复劳动里解放了出来。

声明式UI和命令式UI的区别

首先我们来明确下概念,声明式UI和命令式UI概念到底是怎么样的呢 ?

  • 命令式 UI 就是告诉框架怎么做 ,达成目的的每一步都需要开发者显式的控制,直接操控 UI 元素的创建、更新、销毁和状态变化。

  • 声明式 UI 就是告诉框架做什么 ,具体怎么做我们不关心。开发者仅需描述UI的最终状态,框架会自动处理从当前状态到目标状态的更新逻辑。

这么说可能还是有点抽象,我们可以拿"开车"来做个形象的比喻 :

  • 命令式UI 就像开手动挡 :你需要告诉程序每一个具体操作("挂1档"、"踩离合"、"抬离合给油")。你完全掌控过程,但也承担了所有繁琐和出错的风险,比如说可能会出现熄火。
  • 声明式UI 就像开自动挡 :你只需声明意图("我要加速"),至于引擎如何升档、变速箱如何配合,全部由框架(变速箱)自动完成。你只关注结果和目标,而非实现细节。

了解了声明式UI和命令式UI的概念后,我们接着顺着时间线,好好唠唠UI编程的三个关键阶段,看看它是怎么一步步变成现在这个样子的。

第一阶段:命令式UI的诞生 (1980s)

80年代是个人计算机的"启蒙时代",之前那种对着黑屏敲DOS命令行的交互方式,普通人看着就头大,所以GUI(图形用户界面)应运而生------说白了,就是有窗口、有按钮、有图标,用鼠标点一点就能操作,这可比输命令方便多了。比如微软1985年推出的Windows 1.0,就是早期GUI的代表,一下子让计算机变得"平易近人"了。

但那时候的UI开发,可没现在这么轻松,完全是"命令式"的天下。啥叫命令式?就是你得像指挥机器人一样,一步一步告诉程序"该做什么",从创建窗口到放按钮,再到绑定点击事件,每一个动作都得写代码指令,一点都不能省。

这一阶段的核心代表技术就是Windows API(Win32),这可是当时Windows平台开发的"标配",想做个带窗口的程序,绕不开它。

具体来说,这个阶段的开发有三个很明显的特点:

  1. 过程化创建:代码又长又乱,耦合度拉满
    开发者得从头写到尾,先注册窗口类,再创建窗口,然后显示窗口,还要给按钮绑定事件,代码写得老长了。更头疼的是,UI代码和业务逻辑搅和在一起,比如你想改个按钮的位置,可能得翻好几段代码,改完还怕影响其他功能,简直是牵一发而动全身。
  2. 平台绑定:跨平台?基本等于重写
    UI控件长得啥样、怎么用,全看操作系统的脸色。比如你在Windows上写的按钮,放到Mac上可能就变样了,甚至连点击事件的处理方式都不一样。想跨平台开发?别想了,基本是重新写一遍的节奏。
  3. 关注"如何做":开发者得当"精细工匠"
    你得精确控制每一步,比如窗口的大小、位置,按钮的颜色、字体,甚至窗口刷新的时机,都得自己操心。简单说,你不仅要告诉程序"做什么",还得教它"怎么做",每一个细节都得亲手打磨。

给大家看个简单的例子,你就知道当时写代码有多繁琐了:

c 复制代码
// 1. 注册窗口类(命令1)
WNDCLASS wc = {0};
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = "MyWindowClass";
RegisterClass(&wc);
// 2. 创建窗口(命令2)
HWND hWnd = CreateWindow("MyWindowClass", "Hello UI", WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
                          NULL, NULL, hInstance, NULL);
// 3. 显示窗口(命令3)
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);

就建个空窗口,就得写这么多代码,要是再加个按钮、绑个事件,代码量直接翻倍。

但是Win32 API我们毕竟没用过,再举个Android的例子 : 用Android纯代码编写UI,

其实这种用Android纯代码编写UI就是完全命令式UI的写法,这种写法Android开发者都写过,都知道写起来有多繁琐了。

kotlin 复制代码
LinearLayout rootLayout = new LinearLayout(this);
rootLayout.setOrientation(LinearLayout.VERTICAL);
rootLayout.setLayoutParams(new ViewGroup.LayoutParams());
Button button = new Button(this);
LinearLayout.LayoutParams btnParams = new LinearLayout.LayoutParams();
button.setLayoutParams(btnParams);
button.setText("点击了0次");
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
            count++;
    button.setText(String.format("点击了%d次", count));
}});
rootLayout.addView(button);

第二阶段:命令式UI的优化,标记语言的分离 (1990s-2000s)

到了90年代,互联网开始兴起,网页成了新的UI载体,而桌面端的开发也觉得命令式太麻烦了,于是大家开始想办法"偷懒"------先通过标记语言分离实现UI架构的解耦,再通过封装工具简化命令式操作,这就到了第二个阶段,也是UI开发的"解耦时代"。

标记语言的分离:结构、样式、行为分离

其中一项变革就是声明式标记语言的出现与分层分离,它彻底打破了早期UI开发结构、样式、行为混杂的混乱局面,构建起现代UI开发的基础架构。

这个阶段的核心代表技术就是Web三大件(HTML/CSS/JavaScript),同时桌面端也借鉴了这一思想,诞生了以XML为基础的标记语言(比如WPF的XAML):

  • HTML(1993年):率先实现声明式UI结构描述,无需编写创建元素的命令式代码,直接用标签描述"需要什么UI";
  • JavaScript(1995年):专门负责处理UI交互行为,与HTML结构实现初步分离;
  • CSS(1996年):进一步抽离样式表现,最终形成"结构-样式-行为"的三层分离架构。

这样,HTML就可以用声明式的UI来描述网页界面了,只不过只能描述出初始的静态界面,界面的动态变更还是得借助JavaScript,来处理UI的交互行为,JavaScript这部分还是命令式的UI。

命令式UI的简化:基于标记语言架构的上层封装(jQuery)

折阶段还有一项变更是,原生JavaScript进行命令式DOM操作时,不仅代码繁琐,还需要处理大量浏览器兼容问题(比如IE和Chrome的事件绑定、AJAX请求差异),比如获取一个元素要写document.getElementById('myBtn'),绑定点击事件还要写一堆兼容代码,让人头皮发麻。

为了解决这些问题,各种基于原生JavaScript的封装工具和框架应运而生,核心就是把重复的命令式操作封装起来,让开发者少写冗余代码,提升开发效率。其中最标志性的就是2006年诞生的jQuery,它的口号是"Write Less, Do More"(写得更少,做得更多),简直说到了开发者的心坎里。

但是jQuery本质上还是命令式的写法,只不过是把重复的命令式操作封装起来了。

js 复制代码
// jQuery仍需手动描述操作步骤(命令式)
let count = 0; // 定义计数变量
$('#myBtn').click(function() {
  count++; // 每次点击计数累加
  $(this).text(`点击了${count}次`); // 手动更新按钮文本
});
移动端的延续:布局与逻辑的半分离

2007年iPhone的发布堪称移动互联网的里程碑,移动应用开发一下子火了起来。各大厂商也顺势采纳了"标记语言分离"的思想,搞出了布局层面的声明式,不过逻辑处理还是没跳出命令式的圈子,算是一种"半分离"的状态。

Android 是这一思路的典型代表:它用XML布局文件 来描述UI结构,比如你想加个按钮,直接写<Button android:text="点击了0次" android:layout_width="wrap_content" />就行,不用再写代码手动创建控件。但这只是第一步,后续你还得在Java或Kotlin代码里,用findViewById找到这个按钮的实例,再手动绑定点击事件、更新计数和按钮文本------这些逻辑部分还是得一步步写命令式代码。

举个简单的例子,Android里实现按钮点击计数累加,得这么干:

xml 复制代码
<!-- res/layout/activity_main.xml 声明式布局 -->
<Button
    android:id="@+id/myBtn"
    android:text="点击了0次"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
java 复制代码
// MainActivity.java 命令式逻辑
Button myBtn = findViewById(R.id.myBtn);
int count = 0; // 定义计数变量
myBtn.setOnClickListener(v -> {
    count++; // 每次点击计数累加
    myBtn.setText("点击了" + count + "次"); // 手动更新按钮文本
});

而iOS这边,早期也用XIB或Storyboard(本质是XML格式的标记文件)来做界面布局,同样需要在Objective-C或Swift代码里手动处理控件的事件和状态,和Android的路子大同小异。

这种模式虽然比纯命令式方便了不少,把布局和逻辑拆开来了,但还是得开发者手动同步状态和UI,状态多了之后还是容易乱。

第三阶段:声明式UI的爆发与成熟

时间来到2010年代之后,随着互联网应用越来越复杂,比如电商网站、社交平台,页面元素多、交互复杂,原来的开发方式又遇到了新问题------比如状态管理混乱,UI更新不及时,大型项目维护困难。这时候,声明式UI就彻底爆发了,直接重构了UI开发的玩法。

声明式UI的核心是啥?简单说,你只需要描述"UI和状态的关系",比如"count是0的时候,按钮显示'点击了0次',count加1后,按钮就显示'点击了1次'",至于怎么更新UI、怎么渲染,全交给框架来做,开发者再也不用操心这些细节了。

这一阶段的框架层出不穷,每个都有自己的特色,咱们一个个唠:

React(2013,Facebook):声明式UI的"破局者"

Facebook当时面临一个大问题:社交平台的动态内容多,UI更新频繁,传统方式做起来性能差、维护难。于是2013年,React横空出世,它的两个核心创新------虚拟DOM组件化,直接解决了这个痛点。

虚拟DOM就是先在内存里建一个DOM的"副本",状态变化时,只对比新旧虚拟DOM的差异,然后只更新变化的部分,大大提升了性能。组件化则是把UI拆成一个个独立的小部件(比如按钮、导航栏),可以重复使用,大型项目维护起来就轻松多了。

React用JSX语法,把HTML和JS结合在一起,直接描述UI和状态的映射,代码特别直观。比如这个计数器按钮,你看只需要描述状态和UI的关系,点击事件改状态就行,更新的事框架全包了:

js 复制代码
// 仅描述UI与状态的映射,框架自动处理更新
function ButtonCounter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      点击了{count}次
    </button>
  );
}
Vue.js(2014,尤雨溪):更易上手的声明式框架

React火了之后,2014年,尤雨溪推出了Vue.js,它走的是"渐进式框架"的路子------你可以先用它的核心功能,比如模板和双向绑定,然后根据需要再加路由、状态管理这些插件,不用一下子学完所有东西,对新手特别友好。

Vue早期结合了AngularJS的双向绑定和React的组件化,用的是模板式声明式语法,和传统的HTML很像,前端开发者一看就会。比如这个Vue的计数器,模板部分和HTML几乎一样,状态存在data里,点击直接改count就行,特别简单:

js 复制代码
<template>
  <!-- 声明式模板:UI与count状态绑定 -->
  <button @click="count++">点击了{{ count }}次</button>
</template>

<script>
export default {
  data() {
    return { count: 0 };
  }
};
</script>
Angular(2016,Google):企业级声明式框架

Google在2010年推出了AngularJS,但后来发现它在大型项目里有一些问题,于是在2016年用TypeScript重构了Angular(也就是Angular 2),放弃了原来的双向绑定主导模式,改用组件化、声明式模板、单向数据流的设计。

Angular是个"全家桶"框架,内置了路由、表单、HTTP请求、依赖注入等各种企业级功能,不用自己找第三方插件,所以特别适合开发大型的企业应用,比如银行、电商的后台系统。

Flutter(2017,Google):跨平台的纯声明式框架

前面的框架主要是针对Web的,而移动开发的跨平台一直是个痛点------比如用原生开发iOS和Android,要写两套代码;用混合开发,性能又不行。2017年,Google推出的Flutter解决了这个问题。

Flutter基于Dart语言,采用Widget树的纯声明式设计,所有UI元素都是Widget,而且它有自己的自绘引擎,不依赖操作系统的原生控件,所以iOS和Android上的UI长得一模一样,性能也和原生差不多。状态变化时,Flutter会重新构建Widget树,不过它用Diff算法优化,只更新变化的部分,效率很高。

比如这个Flutter的计数器,把UI拆成StatefulWidget,状态变化时调用setState,框架就会自动重建UI:

dart 复制代码
// 声明式描述按钮的状态与UI
class CounterButton extends StatefulWidget {
  @override
  _CounterButtonState createState() => _CounterButtonState();
}

class _CounterButtonState extends State<CounterButton> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () => setState(() => count++),
      child: Text('点击了$count次'),
    );
  }
}
SwiftUI(2019,Apple):苹果生态的声明式革命

Apple也不甘落后,2019年推出了SwiftUI,这是专门为iOS、macOS、iPadOS等苹果平台设计的声明式UI框架。它直接用Swift语言描述UI,不用再写Storyboard或XIB文件了,而且和Combine框架结合,能轻松实现响应式状态管理。SwiftUI的出现,让苹果原生开发也进入了声明式时代,开发者不用再写一堆繁琐的UIKit代码,效率提升了不少。

Jetpack Compose(2019,Google):Android原生的声明式选择

Google在2019年推出了Jetpack Compose,作为Android的官方声明式UI框架,取代了传统的XML布局。它用Kotlin语言的函数式语法描述UI,状态变化时自动重组,代码比XML简洁多了。比如这个Compose的计数器,用remember保存状态,mutableStateOf实现状态可观察,改状态就自动更UI,特别丝滑:

kotlin 复制代码
/**
 * Compose 声明式的计数器按钮
 * 利用 remember 持久化状态,mutableStateOf 实现状态可观察(变化时自动重组UI)
 */
@Composable
fun CounterButton() {
    // 1. 用 remember 保存状态,避免重组时重新初始化
    // 2. 用 by 委托简化 State.value 的访问(等价于 val count = remember { mutableStateOf(0) },使用时 count.value)
    var count by remember { mutableStateOf(0) }

    // Compose 的 Button 组件(对应Flutter的ElevatedButton)
    Button(
        onClick = { 
            // 点击时修改状态,Compose 会自动重组依赖该状态的UI
            count++ 
        },
        modifier = Modifier.padding(16.dp) // 可选:添加内边距
    ) {
        // 文本显示计数,状态变化时自动更新
        Text(text = "点击了$count次")
    }
}
ArkUI (2021,华为):鸿蒙全场景的声明式框架

华为在2021年推出的ArkUI,是鸿蒙操作系统的核心声明式开发框架,它的核心理念同样是"状态驱动UI",而且还针对鸿蒙的"全场景分布式"特性做了专门优化,能让开发者用一套代码适配手机、平板、手表、车机、智慧屏等多种设备,真正实现了"一次开发,多端部署"。

ArkUI早期支持TS/JS语言,后来又推出了基于TypeScript扩展的ArkTS语言,采用函数式组件+装饰器的语法,让状态与UI的绑定更直观。当状态发生变化时,框架会自动更新对应的UI组件,不用开发者手动操作。比如这个简单的计数器示例,就能看出它的声明式特点:

ts 复制代码
// ArkTS 声明式计数器按钮
@Entry
@Component
struct CounterButton {
  // 定义状态变量,状态变化自动驱动UI更新
  @State count: number = 0;

  build() {
    Column() {
      Button(`点击了${this.count}次`)
        .onClick(() => {
          // 修改状态,UI自动更新
          this.count++;
        })
        .padding(16)
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%');
  }
}

ArkUI的出现,不仅让鸿蒙生态的开发效率大幅提升,也让声明式UI的理念在全场景智能设备领域得到了新的落地。

Compose Multiplatform (2022, JetBrains & Google):基于Compose扩展的跨平台声明式框架

Compose Multiplatform是JetBrains 基于Compose扩展的跨平台声明式框架,不仅仅支持Android,跨平台地支持了Android、iOs、桌面端和Web网页。

ovCompose (2025,腾讯):基于Compose Multiplatform的首个支持鸿蒙的跨平台框架

甚至于2025年,腾讯还基于Compose Multiplatform推出了首个支持鸿蒙的跨平台框架,用来弥补Compose Multiplatform不支持鸿蒙平台的遗憾,便于在国内构建全跨端应用。

写在最后

回顾这几十年的UI编程发展史,其实就是一个不断"解放开发者"的过程:从命令式的步步为营,到标记语言的分离解耦,再到声明式的智能高效,每一次技术的进步,都让我们能把更多精力放在产品的逻辑和体验上,而不是纠结于UI的实现细节。

未来的UI开发会往哪走?可能会更智能,比如AI辅助生成UI代码,或者跨平台、跨端的框架更完善,但核心肯定还是让开发更简单、更高效。

相关推荐
aidou13144 小时前
Android中RecyclerView实现多级列表
android·recyclerview·多级列表·layoutmanager
青风行4 小时前
Android从入门到进阶
android
方白羽5 小时前
Android 开发中,准确判断应用处于“前台(Foreground)”还是“后台(Background)
android·app·客户端
Mart!nHu5 小时前
Android 10&15 Framework 允许设置系统时间早于编译时间
android
编程之路从0到16 小时前
ReactNative新架构之Android端TurboModule机制完全解析
android·react native·源码阅读
iloveAnd7 小时前
Android开发中痛点解决(二)兼容性:AndroidX和gradle版本的兼容性
android·兼容性·androidx
stevenzqzq8 小时前
DataStore基本使用教程
android
LawrenceMssss8 小时前
由于创建一个完整的App涉及到多个层面(如前端、后端、数据库等),并且每种语言通常有其特定的用途(如Java/Kotlin用于Android开发,Swift/Objective-C用于iOS开发,Py
android·java·ios
chen_mangoo9 小时前
HDMI简介
android·linux·驱动开发·单片机·嵌入式硬件