¿Por qué el código declarativo de Compose es el más conciso? Comparación de sintaxis de Compose/React/Flutter/SwiftUI

prefacio

Comopse pertenece al mismo marco de IU declarativo que React, Flutter y SwiftUI, y tiene el mismo concepto de diseño y principios de implementación similares, pero el diseño de la API de Compose es más conciso. Este artículo compara el código de estos marcos y siente la alta eficiencia de código de Compose.

1. Componentes sin estado

La característica básica de la interfaz de usuario declarativa es crear vistas basadas en componentes reutilizables. El proceso de desarrollo de la interfaz de usuario declarativa es esencialmente el proceso de definición de varios componentes de la interfaz de usuario. Los componentes generalmente se clasifican en componentes sin estado y componentes con estado.

React proporciona dos tipos de definiciones de componentes: componentes de clase y componentes funcionales:

//JS
function Greeting(props) {
  return <p>Hello, {props.name}</p>;
}
//JS
class Greeting extends React.Component {
  render() {
    return <p>Hello, {this.props.name}</p>;
  }
}

Los datos del componente de la función se pasan a través de los parámetros de la función JS; el componente de la clase se establece a través del atributo de etiqueta de JSX y se this.propslee . propsTenga en cuenta stateque es de solo lectura e inmutable, que es la diferencia esencial entre Stateless y Stateful. Los componentes funcionales son más concisos en el código, evitando el código de plantilla que traen las definiciones de clase, por lo tanto, los componentes funcionales se usan cada vez más en React.

Los componentes de clase y los componentes funcionales también dividen los marcos de IU declarativos en dos escuelas, Flutter y SwiftUI pertenecen a la primera y Compose pertenece a la segunda. Esto también determina que la definición de los componentes de Compose será más concisa.

Echemos un vistazo a los componentes sin estado de Flutter y SwiftUI respectivamente

//Dart
class Greeting extends StatelessWidget {
  const Greeting({required this.name});

  final String name;

  @override
  Widget build(BuildContext context) {
    return Text("Hello, $name");
  }
}

Flutter usa las características heredadas de los componentes de clase, deriva Stateless personalizado de StatelessWidget y luego define un constructor para pasar datos. build(BuildContext)mediante la creación de instancias de componentes secundarios y la creación de la interfaz de usuario. Gracias a Dart2, la newpalabra clave se puede omitir, de lo contrario, el código de llamada del constructor estará más inflado.

//Swift
struct Greeting: View {
    var name: String
    
    var body: some View {
        Text("Hello, \(name)")
    }
}

严谨地说 SwiftUI 组件不是类组件而是”结构体组件”。Class 是引用类型,而 Struct 是值类型。使用结构体定义组件有助于提升 UI 的不可变性,也是从面向对象向函数式编程过度的一种体现,但是结构体组件从形式上更接近类组件,不如函数组件简洁。

接下来看一下 Compose 的 Stateless:

//Kotlin
@Composable
fun Greeting(name: String) {
    Text("Hello $name")
}

Compose 的代码明显更简洁,几乎就是一个普通的函数定义,唯一的区别就是增加了一个 @Composable 注解,这个注解在编译期生成许多辅助框架运行的代码,开发者可以少些很多代码,从代码量的角度来看,次注解的性价比非常高。

即使同为函数组件的 React 相比,Compose 也更胜一筹,Composable 无论定义还是使用都是基于 Kotlin,使用体验更一致。而 React 的函数式组件需要在 JSX 中使用,虽然符合前端开发习惯,但是但从代码复杂度上来说是不友好的。另外 Composable 没有返回值,连 return 都省了,更简洁。

Compose React

2.Stateful 组件

React 的函数式组件使用 Hooks API 定义状态

//JS
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <><button onClick={() => setCount(count + 1)}>
        {count}
      </button></>
  );
}

React Hooks 开创了声明式 UI 状态管理的新方式,相对于传统的基于父类方法的方式代码效率得到大幅提升。Compose 的状态管理以及各种副作用 API 的设计灵感也来自 React Hooks. (参考:相似度99%?Jetpack Compose 与 React Hooks API对比

Flutter 中自定义 Stateful 组件是比较繁琐的,首先 StatefulWidget 返回一个 State d对象,Widget 定义在 State 中。

//Dart
class Counter extends StatefulWidget {
  @override
  _Counter createState() => _Counter();
}

class _Counter extends State<Counter> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: _incrementCounter,
      child: Text("$_counter"),
    );
  }
}

State 的变化触发 Widget 的重新构建,这确实贯彻了状态驱动 UI 的设计原则,但是增加了心智理解的成本。当然,也有诸如 Flutter Hooks 这样的三方库可供使用,实现类似 React Hooks 的效果:

//Dart
class Counter extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final counter = useState(0);

    return TextButton(
      onPressed: () => counter.value++,
      child: Text("${counter.value}"),
    );
  }
}

SwiftUI 的 Stateful 的定义比较简洁:

//Swift
struct Counter: View {
    @State var count = 0
    
    var body: some View {
        Button(
            action: { count += 1 },
            label: { Text("\(count)")}
        )
    }
}

使用 @State 注解定义一个成员变量,变量的变化可以自动触发界面刷新。

最后看一下 Compose 的 Stateful:

//Kotlin
@Composable
fun Counter() {
    val count by remember { mutableStateOf(0) }
    Button(
        onClick = { count++ }
    ) {
        Text("${count}")
    }
}

Compose 的 remember 本质也是一种 Hooks 函数,但是 Compose 的 Hooks 是用起来比起 React 更方便,在 React 中是用 Hooks 有诸多限制,例如下面这些用法都是不允许的。

  • 将 Hooks 函数放在条件分支里
//JS
if (flag) {
  const [count, setCount] = useState(0);
  ...
}

在子组件定义时,使用 Hooks 函数

//JS
return (
  <div>
    {
      const [count, setCount] = useState(0);
      ...
    }
  </div>
)

在 Composable 中这些都不是问题,因为 Compose 独有的 Positional Memoization 机制,可以根据静态的代码位置存储状态,不会受到运行时的分支条件变化的影响。另外 Compose 所有代码都是同构的,不会存在 JSX 无法插入 Hooks 的窘境,所以上面两种 React 中的禁忌在 Compose 中都可以实现:

//Kotlin
if (flag) {
    val count by remember { mutableStateOf(0) }
    ...
}
//Kotlin
Column {
    val count by remember { mutableStateOf(0) }
    ...
}

3. 控制流语句

我们经常有根据分支条件显示不同组件的需求,那么各个框架是如何在声明式语法中中如何融入 if/for 等控制流语句的呢?

Compose 的函数式组件在这方面有天然优势,构建 UI 的本质就是一个函数实现的过程,过程中可以自然地插入控制流语句

//Kotlin
@Composable
fun List(value: List<Data>) {
    Column {
        Header()
        if (value.isEmpty()) {
            Empty()
        } else {
            value.forEach {
                Item(it) 
            }
        }
    }
}

上面的 Compose 例子中,通过 if..else 显示不同结果,当数据不为空时,使用 for 循环依次展示,代码非常直观。

反观 Flutter ,基于类组件的声明式 UI 本质上是不断构建对象的过程子组件通过构造参数传入,这个工程中插入控制流会比较复杂,上面同样的 UI 在 Flutter 中写会像下面这样:

//Dart
@override
Widget build(BuildContext context) {
  List<Widget> widget;
  if (value.isEmtpy) {
      widget = Empty();
  } else {
      for (var i in value) {
          widget.add(i);
      }
  }
  return Column(children: [
    Header(),
    ...widget
  ]);
}

所幸,Dart 2.3 之后新增了 Collection-ifCollection-for,可以在 List 构造中使用 if/for,代码大大简化:

//Dart
@override
Widget build(BuildContext context) {
  return Column(children: [
    Header(),
    if (value.isEmpty) Empty(),
    for (var i in value) Item(i)
  ]);
}

SwiftUI 原本应该像类组件那样通过对 Struct 的初始化添加子组件,但是它提供了 ViewBuilder 这样的机制,可以使用 DSL 进行 UI 构建,和 Compose 几乎无异

//Swift
var body: some View {
    VStack {
        Header()
        if value.isEmpty {
            Empty()
        }
        ForEach(value) {
            item inItem(item)
        }
    }
}

需要注意 ViewBuilder 中不能使用普通的控制流语句,ForEach 是针对 SwiftUI 定制的方法。

无论是 Flutter 还是 SwiftUI 他们的控制流语句都需要依赖一些定制语法或者语法糖,不像 Compose 那样朴实,代码的可复用性也自然会受到影响。

最后简单看一下 React 吧,同样的逻辑实现如下

//JS
function List(value) {
    return (
        <div><Header />
            { value.isEmpty() && <Empty /> }
            { value.map((item) => <Item value={item} />) }
        </div>
    )
}

虽说是函数组件,但是添加子组件的逻辑不能用纯 JS 实现,需要在 JSX 定义,幸好 JSX 对这样的控制流逻辑也有一些支持。

4. 生命周期

声明式 UI 中都针对组件在视图树上的挂载/卸载定义了生命周期,并提供了响应 API。

React 类组件通过类的成员方法提供生命周期回调,我们重点看一下函数组件的生命周期回调

//JS
useEffect(() => {
  const callback = new Callback()
  callback.register()
  return () => {
    callback.unregister()
  };
}, []);

useEffect 也是一种 Hooks 函数,我们可以利用它监听组件的生命周期。最后返回的 lambda 是可以作为组件卸载时的回调。

Compose 参考 useEffect 提供了一系列副作用 API,以 DisposableEffect 为例

//Kotlin
DisposableEffect(Unit) {
    val callback = Callback()
    callback.register()
    onDispose {
        callback.unregister()
    }
}

设计上完全致敬 Hooks,最后 onDispose 是 Composable 从 Composition 中退出时的回调。

Flutter 作为类组件,自然是通过继承自父类的方法回调生命周期

//Dart
class Sample extends StatefulWidget {
  @override
  _State createState() {
    return _State();
  }
}

class _State extends State<Sample> {
  final Callback _callback = Callback();

  @override
  Widget build(BuildContext context) {
    return ...;
  }

  @overridevoid initState() {
    super.initState();
    callback.register()
  }

  @overridevoid dispose() {
    super.dispose();
    callback.unregister()
  }
}

当然,使用前面提到的 Flutter Hooks 的话,可以达到 React 与 Compose 的效果。

SwiftUI 的结构体组件没有继承,所以通过 onAppearonDisappear 设置生命周期回调。相对于继承的方式更加简洁,但是它只能设置子组件的回调,无法对当前组件进行设置。

//Swift
struct Sample: View {
    private let callback = Callback()

    var body: some View {
        Component()
            .onAppear(perform: {
                callback.register()
            })
            .onDisappear(perform: {
                callback.unregister()
            })
    }
}

综上,React 和 Compose 的 Hooks 风格的生命周期回调最为简洁,因为挂载/卸载的回调可以在一个函数中完成,例如当我们要往一个 callback 实例上注册/注销回调时,可以闭环完成操作,不必额外存储这个 callback 实例。

5. 装饰/样式

对比一下组件样式的设置上 API 的区别,以最常用的 backgroundpadding 等为例。

React 基于 JSX 和 CSS-in-JS,可以像写 HTML + CSS 那样设置组件样式,可以比较好地实现 Style 与 Component 的解耦

//JS
const divStyle = {
  padding: '10px',
  backgroundColor: 'red',
};
return <div style={divStyle}>Hello World</div>;

Compose 通过 Modifier 为 Composable 设置样式

//Kotlin
Text(
    text = "Hello World",
    modifier = Modifier
        .background(Color.Red)
        .padding(10.dp)
)

Flutter 通过 Widget 的构造参数设置样式,使用比较简单,但是不具备 Modifier 的灵活性,不同 Widget 的 Style 无法复用。

//Dart
Container(
  color: Colors.red,
  padding: const EdgeInsets.all(10),
  child: Text("Hello World"),
)

SwiftUI 的样式设置是基于组件实例的链式调用,非常简单

//Swift
Text("Hello World")
  .padding(10)
  .background(Color.red)

综上,在样式设置上各家的 API 风格都比较简单,但是 Compose 的 Modifier 仍然具有不可比拟的优势,比如类型安全和容易复用等,Modifier 本身也是一种非常好的设计模式。

总结

前面基于代码片段进行了一些对比,最后以 Counter Demo 为例,看一个完整功能下 Flutter、Compose 和 Swift 的代码对比,React 与其他三者代码风格差异较大,就不参加比较了。

Flutter Compose SwiftUI

可以感觉到 Compose 代码最简洁也最直观,SwiftUI 通过 ViewBuilder 机制也可以实现与 Compose 类似的 DSL,表现也非常不错,Flutter 由于模板代码较多,在简洁程度上表现最差。

Kotlin、Dart 和 Swift 的语法非常相近,所以抛开语言层面的差异,Compose 的优势主要还是来自于其采用了函数式的组件形式并借鉴了 React Hooks 的设计思想。可以说 Compose 诞生于 React 的肩膀上,并借助 Kotlin 将代码效率提升到一个新高度。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

Supongo que te gusta

Origin juejin.im/post/7121896120543870990
Recomendado
Clasificación