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代码,或者跨平台、跨端的框架更完善,但核心肯定还是让开发更简单、更高效。

相关推荐
符哥20086 分钟前
Fastjson2.X 使用详解
android·java
月明泉清13 分钟前
Android中对于点击事件的深度梳理(三)
android
电饭叔21 分钟前
DataFrame和 Series 索引
android·python
lexiangqicheng29 分钟前
【全网最全】React Native 安卓原生工程结构与构建机制深度解析
android·react native·react.js
数据蜂巢1 小时前
MySQL 8.0 生产环境备份脚本 (Percona XtraBackup 8.0+)
android·mysql·adb
jingling5551 小时前
uniapp | 基于高德地图实现位置选择功能(安卓端)
android·前端·javascript·uni-app
fatiaozhang95271 小时前
晶晨S905L/S905LB-通刷-slimbox 9.19-Mod ATV-安卓9-线刷固件包
android·电视盒子·刷机固件·机顶盒刷机
爱怪笑的小杰杰1 小时前
UniApp 桌面应用实现 Android 开机自启动(无原生插件版)
android·java·uni-app
符哥20081 小时前
Fresco2.X 框架完整使用详解(Android Kotlin)
android
TheNextByte11 小时前
如何在Android上恢复已删除的联系人
android