Compose聲明式代碼語法對比React?Flutter?SwiftUI
前言
Comopse 與 React、Flutter、SwiftUI 同屬聲明式 UI 框架,有著相同的設計理念和相似的實現原理,但是 Compose 的 API 設計要更加簡潔。
本文就這幾個框架在代碼上做一個對比,感受一下 Compose 超高的代碼效率。
1.Stateless 組件
聲明式 UI 的基本特點是基于可復用的組件來構建視圖,聲明式 UI 的開發(fā)過程本質上就是各種 UI 組件的定義過程。組件在類型上一般分為無狀態(tài)的 Stateless 組件和有狀態(tài)的 Stateful 組件。
React 提供了類組件,函數式組件兩種組件定義方式:
//JS function Greeting(props) { return <p>Hello, {props.name}</p>; }
//JS class Greeting extends React.Component { render() { return <p>Hello, {this.props.name}</p>; } }
函數組件的數據通過 JS 函數參數傳遞;類組件通過 JSX 的標簽屬性設置,并通過 Class 的 this.props
讀取。注意 props
不同于 state
, 它是只讀的不可變化,這也是 Stateless 和 Stateful 的本質區(qū)別。在代碼上函數式組件更加簡潔,避免了類定義帶來的模板代碼,因此,函數式組件在 React 中的使用占比越來越高。
類組件和函數組件也將聲明式 UI 框架劃分為兩個流派,Flutter 和 SwiftUI 屬于前者,而 Compose 屬于后者。這也從基礎上決定了 Compose 的組件的定義將更加簡潔。
讓我們分別看一下 Flutter 和 SwiftUI 的 Stateless 組件
//Dart class Greeting extends StatelessWidget { const Greeting({required this.name}); final String name; @override Widget build(BuildContext context) { return Text("Hello, $name"); } }
Flutter 使用類組件繼承的特性,通過 StatelessWidget 派生自定義 Stateless,然后定義構造函數用來傳遞數據。build(BuildContext)
方法中通過實例化子組件并構建 UI 。這還得感謝 Dart2 中對 new
關鍵字可以省略,不然構造函數的調用代碼會更顯臃腫。
//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 的代碼明顯更簡潔,幾乎就是一個普通的函數定義,唯一的區(qū)別就是增加了一個 @Composable 注解,這個注解在編譯期生成許多輔助框架運行的代碼,開發(fā)者可以少些很多代碼,從代碼量的角度來看,次注解的性價比非常高。
即使同為函數組件的 React 相比,Compose 也更勝一籌,Composable 無論定義還是使用都是基于 Kotlin,使用體驗更一致。而 React 的函數式組件需要在 JSX 中使用,雖然符合前端開發(fā)習慣,但是但從代碼復雜度上來說是不友好的。另外 Composable 沒有返回值,連 return
都省了,更簡潔。
Compose
React
2.Stateful 組件
React 的函數式組件使用 Hooks API 定義狀態(tài)
//JS function Counter() { const [count, setCount] = useState(0); return ( <><button onClick={() => setCount(count + 1)}> {count} </button></> ); }
React Hooks 開創(chuàng)了聲明式 UI 狀態(tài)管理的新方式,相對于傳統(tǒng)的基于父類方法的方式代碼效率得到大幅提升。Compose 的狀態(tài)管理以及各種副作用 API 的設計靈感也來自 React Hooks. (參考:相似度99%?Jetpack Compose 與 React Hooks API對比)
Flutter 中自定義 Stateful 組件是比較繁瑣的,首先 StatefulWidget 返回一個 State d對象,Widget 定義在 State 中。
State 的變化觸發(fā) Widget 的重新構建,這確實貫徹了狀態(tài)驅動 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
注解定義一個成員變量,變量的變化可以自動觸發(fā)界面刷新。
最后看一下 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 機制,可以根據靜態(tài)的代碼位置存儲狀態(tài),不會受到運行時的分支條件變化的影響。另外 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 的函數式組件在這方面有天然優(yōu)勢,構建 UI 的本質就是一個函數實現的過程,過程中可以自然地插入控制流語句
//Kotlin @Composable fun List(value: List<Data>) { Column { Header() if (value.isEmpty()) { Empty() } else { value.forEach { Item(it) } } } }
上面的 Compose 例子中,通過 if..else
顯示不同結果,當數據不為空時,使用 for
循環(huán)依次展示,代碼非常直觀。
反觀 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-if
和 Collection-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 函數,我們可以利用它監(jiān)聽組件的生命周期。最后返回的 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 的結構體組件沒有繼承,所以通過 onAppear
和 onDisappear
設置生命周期回調。相對于繼承的方式更加簡潔,但是它只能設置子組件的回調,無法對當前組件進行設置。
//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
實例上注冊/注銷回調時,可以閉環(huán)完成操作,不必額外存儲這個 callback 實例。
5. 裝飾/樣式
對比一下組件樣式的設置上 API 的區(qū)別,以最常用的 background
,padding
等為例。
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 仍然具有不可比擬的優(yōu)勢,比如類型安全和容易復用等,Modifier 本身也是一種非常好的設計模式。
總結
前面基于代碼片段進行了一些對比,最后以 Counter Demo 為例,看一個完整功能下 Flutter、Compose 和 Swift 的代碼對比,React 與其他三者代碼風格差異較大,就不參加比較了。
Flutter
Compose
SwiftUI
可以感覺到 Compose 代碼最簡潔也最直觀,SwiftUI 通過 ViewBuilder 機制也可以實現與 Compose 類似的 DSL,表現也非常不錯,Flutter 由于模板代碼較多,在簡潔程度上表現最差。
Kotlin、Dart 和 Swift 的語法非常相近,所以拋開語言層面的差異,Compose 的優(yōu)勢主要還是來自于其采用了函數式的組件形式并借鑒了 React Hooks 的設計思想??梢哉f Compose 誕生于 React 的肩膀上,并借助 Kotlin 將代碼效率提升到一個新高度。
以上就是Compose聲明式代碼語法對比React Flutter SwiftUI的詳細內容,更多關于Compose語法對比React Flutter SwiftUI的資料請關注腳本之家其它相關文章!