前言
近几年,大前端越来越流行声明式UI+响应式编程的模式,如React、Vue、Flutter、Compose等,通过分析主流的语言框架的写法,提炼声明式UI+响应式编程的核心。
通过本篇文章,你将了解到:
- 命令式UI、声明式UI、响应式编程联系与差异
- 主流语言/框架的典型编码示例
- 声明式UI+响应编程的本质
1. 命令式UI、声明式UI、响应式编程联系与差异
命令式UI编程
以Android设置。 通常更新UI会固定执行以下操作:
kotlin
class SecondActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
// 1. 获取控件(View)对象
val textView = findViewById<TextView>(R.id.tv_welcome)
// 2. 设置控件(View)属性
textView.setTextColor(Color.BLACK)
}
}
实际就分三步(前两步需要我们自己控制)。
命令式UI编程(Imperative UI Programming)是一种通过显式地操作视图对象及其状态来构建用户界面的方式。开发者需要手动控制UI组件的创建、更新和销毁过程
声明式UI编程
同样功能,我们稍微改造一下,使用Android里的Compose。
kotlin
class SecondActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// 使用Compose布局替代传统View
WelcomeScreen()
}
}
}
// 声明式UI组件
@Composable
fun WelcomeScreen() {
MaterialTheme {
// 替代TextView,设置文本和颜色
Text(
text = "Welcome to My Compose App2",
color = Color.Black
)
}
}
与命令式UI最大的不同点:声明式UI无需显式地持有UI对象,只需要关注UI状态结果即可。
声明式(Declarative)UI是一种通过描述UI应该是什么样子,而不是如何构建它的编程方式。开发者只需要声明当前界面的状态和结构,框架会自动处理状态变化后的更新与渲染。
Compose 声明式UI框架通过一个个被@Composable修饰的函数组合起来,最终描述UI的状态。
响应式编程
响应,顾名思义,需要弄清两个点,谁响应了谁? 很多时候我们代码的顺序都是按时间的先后顺序执行,A调用B,B执行结束后再调用C。 考虑另外的场景,A调用B,因为B比较耗时,A不相等,于是它告诉B:我先干别的(执行C),你弄完给我信号。等B完成后通知A,这个过程就是:
B 响应了 A
很显然,上述场景是典型的观察者模式,其本质是观察者注册了一个回调给被观察者,当被观察者满足条件后通过回调告诉观察者,最终被观察者响应了观察者。 用kotlin代码简单实现如下:
kotlin
//观察者
class Observer {
//注册回调
fun register() {
val subject = Subject()
subject.register(object : Callback {
//回调
override fun notify() {
}
})
}
}
interface Callback{
fun notify()
}
//被观察者
class Subject {
private val observers = mutableListOf<Callback>()
fun register(callback: Callback) {
observers.add(callback)
thread {
//模拟异步通知
Thread.sleep(3000)
notifyObservers()
}
}
fun unregister(callback: Callback) {
observers.remove(callback)
}
private fun notifyObservers() {
observers.forEach {
it.notify()
}
}
}
你可能会说:难道这就是响应式编程?说对了一半,观察者模式是最简单的响应式编程形式,也是其它响应式的基础。 我们想要的效果是:
只需要一个对象,观测和通知都在这个对象上进行,也就是说将观察者和被观察者内置到这个对象的实现里,外部只需要关注怎么监听响应和怎么产生响应即可
因此,我们可以将响应式编程概括如下:
响应式编程(Reactive Programming)是一种基于异步数据流和变化传播的编程范式。它强调通过声明式的方式处理随时间变化的数据流,并自动将这些变化传播到所有依赖该数据的部分
声明式UI+响应式编程
声明式UI声明了UI结构,比如我们的Text需要动态变化,那么它绑定的数据一定是一个可变的数据。当数据发生变化的时候,UI要能感知到,如此一来当数据发生变化时,UI就会自动变化,我们只需要关注数据的变化和UI的声明,整个过程就简洁了许多。 还是以Compose为例:
kotlin
// 声明式UI组件
@Composable
fun WelcomeScreen() {
var name by remember { mutableStateOf("My Compose App2") }
MaterialTheme {
Column {
// 替代TextView,设置文本和颜色
Text(
text = "Welcome to $name",
color = Color.Black
)
// 新增按钮,点击后更改name的值
Button(onClick = { name = "Updated Name" }) {
Text(text = "Update Name")
}
}
}
}
可以看出,我们现在只有一个name对象,Text依赖于name,当点击按钮更改name的值后,Text会自动刷新。 因此当声明式UI+响应式编程两者结合时:
- 是现代大前端开发的主流做法
- UI作为观察者,数据作为被观察者,中间的连接者即为响应式框架
- 通常称声明式UI+响应式编程为UI的状态管理
2. 主流语言/框架的典型编码示例
接下来我们简单分析主流语言/框架的声明式UI+响应式编程的典型实践。 涉及到: 三大原生平台:Android(Compose),iOS(SwiftUI),鸿蒙(ArkTS) 前端双子星:React,Vue 跨平台UI框架:Flutter
Android(Compose)
前面已经陆续以Compose为例分析过,此处就不再重复,需要注意的点是:
- Compose 声明式UI以函数为基础构建(函数是Kotlin里的一等公民,基础中的基础)
- 父布局通过{}包裹子布局
- 响应式是框架本身自带方法,也可以借助Flow来实现响应监听
iOS(SwiftUI)
以下是Swift代码。
swift
import SwiftUI
struct ContentView: View {
@State private var count = 0
var body: some View {
VStack {
Text("你点击了 $count) 次")
.font(.largeTitle)
.padding()
Button(action: {
self.count += 1
}) {
Text("点击我")
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(8)
}
}
.padding()
}
}
想要一个属性被观测,可以使用@State进行修饰,Text声明时绑定了count,当被@State修饰的count发生变化时,Text将自动刷新。 值得注意的点是:
- 响应式是框架本身自带的
- 使用注解(装饰器)修饰变量以期达到响应式的效果
- 父布局通过{}包裹子布局
鸿蒙(ArkTS)
以下是ArkTS代码。
swift
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
RelativeContainer() {
Text(this.message)
.id('HelloWorld')
.fontSize($r('app.float.page_text_font_size'))
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.onClick(() => {
this.message = 'Welcome';
})
}
.height('100%')
.width('100%')
}
}
也是通过注解(装饰器)来修饰可观测的变量。 可以看出,从编码习惯上来看,和SwiftUI比较类似。
值得注意的点是:
- 响应式是框架本身自带的
- 使用注解(装饰器)修饰变量以期达到响应式的效果
- 父布局通过{}包裹子布局
React
react
import React, { useState } from 'react';
const App2: React.FC = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<>
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
textAlign: 'center'
}}>
<h1>React 响应式编程和声明式UI演示</h1>
<p>当前计数: {count}</p>
<button onClick={increment}>增加</button>
<button onClick={decrement}>减少</button>
</div>
</>
);
};
export default App2;
前端的声明式UI更接近HTML语法,毕竟它们脱胎于HTM。 通过 {count}声明了一个UI段落,核心是通过useState(0)构造出一个可读可写的属性count。 声明式UI绑定观测count(可读),当按钮点击后修改count(setCount)的值(可写),UI自动刷新。
- 与之前Compose/SwiftUI/ArkTS 不同的是,React将可观测值的读写分离了出来
- 父布局通过<>包裹子布局
从此处也可以清晰感知到,前端和移动端在声明式UI的写法上还是有很大的差异。
Vue
同为前端双子星之一,Vue和React有不少想通的地方。
vue
<script setup>
import { ref, onMounted } from 'vue'
// reactive state
const count = ref(0)
// functions that mutate state and trigger updates
function increment() {
count.value++
}
// lifecycle hooks
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
Vue是将JS和UI分离,分别为Script与template。 使用ref修饰待观测的变量,当点击button时更改count的值,而该button在声明时绑定了count,因此此时会自动刷新UI。
- 与之前Compose/SwiftUI/ArkTS 有点类似,都是包装变量
- 父布局通过<>包裹子布局
Flutter
dart
class Test5 extends StatefulWidget {
@override
_Test5State createState() => _Test5State();
}
class _Test5State extends State<Test5> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Stateful Widget Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
同样的是声明式UI,与之前其它框架不同的是:Flutter并没有对counter进行单独的包装,Text绑定counter是读取counter的过程,_incrementCounter()是写counter。 当修改了变量的值后,必须要显示的调用setState()方法触发build的执行,最后UI才会刷新,并且刷新的是整个页面,它不像其它框架一样能够针对某个变化的属性刷新某个UI组件。 因此,Flutter官方自带的状态管理并不是完整的响应式编程
当然,为了解决此问题,Flutter社区涌现了许多优秀的状态管理库,如Provider、Riverpod、Bloc、GetX、Redux等,我们以GetX为例,改造上面的代码:
dart
class _Test5State extends State<Test5> {
var _counter = 0.obs;
void _incrementCounter() {
_counter.value += 1;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Stateful Widget Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Obx(()=>
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
此时,将counter变量使用.obs包装起来,在声明式UI那使用Obx包装绑定的Text和counter,而后直接修改counter的值,UI就会自动刷新。 值得注意的是:
- Flutter 需要借助三方库实现完整的响应式状态管理
- Flutter 更多的使用具名参数声明UI,使用children字段包裹子布局,因此代码看着比较多,当UI层次复杂时,嵌套也比较深
3. 声明式UI+响应编程的本质
声明式UI总结:
- 专注于初始构造UI,弱化开发者对UI对象的管理,侧重于配置。
- 对需要动态更改的UI状态,需要绑定可观测的变量/属性。
响应编程总结:
- 基础是观察者模式。
- 通过将观察者模式封装起来,UI组件作为观察者,绑定的可变变量/属性作为被观察者,当变量/属性变化时自动刷新UI,完成响应式更新UI过程。
当然,虽然各家都有自己的状态管理,但社区也不乏存在许多优秀的开源框架,他们或多或少地满足了工程化实践里的现实需求,优势和劣势并存、大家可以根据自己的实际场景选择不同的状态管理库。 以下为一些常用的状态管理库:
Jetpack Compose:Mobius、Koin SwiftUI:ReduxSwift、ReSwift React:Redux、MobX Vue:MobX Flutter:Provid、Bloc、Riverpod、GetX
好了,以上就是现代大前端最简单的画UI过程,当然,这只是每个领域的冰山一角,更多内容还需要深入实践。 不过万变不离其宗,即使再出现其它的语言和框架,都绕不开声明式UI+响应式编程,了解了其核心,我们在切入其它领域时也能事半功倍。
那么问题来了,你觉得以上语言/框架,谁借鉴了谁,你觉得哪个写起来更爽?
如果觉得本文有那么一点点帮助,请一键三连哦~