聊聊类组件到函数组件的变迁

2022-11-30 15:00:08 浏览数 (1)

最近一直在学习 React,在看到 React Hooks 一章时联想到 Compose ,简直有着异曲同工之处,他们都是由 UI 组件、State 状态、Effect 副作用构成,而且,Android 端很多优秀的架构思路都来源于前端,适当性的学习些前端知识,反而更能容易理解当下 Android 原生的架构,这也是我一直推荐大家有时间也学习一下前端的原因,本期主要聊聊 Android 原生与 React 的对比,总结了类组件与函数组件的不同。

1、基于类组件的对比

原生

对于原生 Android 来说,通过 Activity 类来承载当前界面的 UI ,例如如下示例:

代码语言:javascript复制
class HomeActivity extends Activity{
   
   private var textView:TextView ?= null

   private val vm:ViewModel by viewModels<XXXViewModel>()
    
   fun onCreate(){
       textView = findViewById(R.id.textview);
       // 发起请求
       vm.request();
       vm.observer(this){
  		 textView.text = it  // 更新 UI
       }   
   }
}

这还是一个比较简单的例子,当业务越来越复杂,最后你会发现,虽然项目是按照 MVVM 结构来写,但依然控制不住整个 Activity 充斥着各种请求和 observer。当然,也有人用 MVI 的方式来解决这个问题。

React

React 相比较原生而言会有点不同,虽然都是基于类组件开发,但 React 是基于 React.Component,它更像是原生里面的 View,继承自这个 View 来写各种逻辑,然后再将 View 设置到 XML 中,供 Activity 来加载绘制,他们之间的关系就像这样:

但 React.Component 相比较 View 又拥有更丰富的生命周期:

生命周期

React.Component

原生 View

组件挂载

componentDidMount()

onAttachedToWindow()

组件更新

componentDidUpdate()

组件卸载

componentWillUnmount()

onDetachedFromWindow()

...

...

React 示例如下:

代码语言:javascript复制

class HomeWidget extends React.Component {
  ...
  render() {
    return (
      <div>
        <button>"Hello World"</button>
      </div>
    )
  }
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<HomeWidget/>);

与 React 类组件非常相似的还有 Flutter,这两者可以对比着学习

2、基于函数组件的对比

原生

原生在拥有 Jetpack Compose 之后,也具备了像前端那样,基于函数式组件来描述当前 UI 界面的能力,如下是一个累加的组件:

代码语言:javascript复制
@Composable
fun HomeWidget() {
    var count by remember {
        mutableStateOf(0)
    }
    Column {
        Text(
            text = "$count",
            modifier = Modifier.clickable { count   }
        )
    }
}

React

React 在 16.8 版本引入了 React Hooks,可以基于函数式来代替原来的类组件,如下也是一个累加的组件:

代码语言:javascript复制
function HomeWidget() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count   1)}>Click</button>
    </div>
 );
}

结合 Compose 与 React 函数组件的对比来看,两者区别不大,例如 State 状态的对比:

React

Compose

State 状态

useState()

mutableStateOf()

那函数式组件相比较类组件拥有哪些好处呢?

  • 更轻量,不用去写 class
  • 代码更简洁,逻辑更内聚

但函数式组件还有一个问题需要解决,在类组件中,我们有原生 Activity 的 onCreate、onDesotry 等生命周期函数,在 React.Component 中,我们有 componentDidMount、componentWillUnmount 等生命周期函数,那基于函数式的组件,他是如何在函数中感知生命周期呢?那这就要引入 Side-effects(附带效应) 概念了。

3、基于附带效应的对比

对于函数副效应来说,赋予组件拥有如下三种生命周期感知能力即可:

  • 组件挂载
  • 组件更新
  • 组件卸载

原生

Compose 提供了多个 Effect,但这里我们主要讲两个涉及到生命周期的 Effect

  • LaunchedEffect
  • DisposedEffect

这两者的功能对比如下:

Effect

可感知的生命周

是否支持协程

能力

LaunchedEffect

组件挂载、组件更新

支持

在组件中更安全的调用挂起函数,退出组合时会自动取消协程

DisposedEffect

组件挂载 、组件更新 、组件卸载

不支持

可以监听组件的退出

1、模拟 LaunchedEffect 仅感知组件挂载的能力,例如请求网络获取到数据后设置给 state,然后通知界面刷新:

代码语言:javascript复制
@Composable
fun HomeWidget() {
    var response by remember {
        mutableStateOf("")
    }

    LaunchedEffect(true) {
        // todo 模拟请求网络
        delay(1000)
        response = "hello world"
        // 打印 log
        Log.e("TAG", "LaunchedEffect")
    }

    Column {
        Text(text = response),
        // ... 省略累加控件
    }
}

在进入组合项时,LaunchedEffect 设置为 true,使其不具备监听任何状态变化的能力(remember),在延迟 1s 后会打印 Log,之后无论怎么操作其他控件都不会使其响应。除非组合项卸载并重进进入挂载状态才会触发,例如移除组件,然后又重新添加了该组件这种情况。

2、模拟 LaunchedEffect 感知 组件挂载、组件更新的能力,例如模拟加载更多操作,触发加载更多就去请求网络数据:

代码语言:javascript复制
@Composable
fun HomeWidget() {
    var count by remember {
        mutableStateOf(0L)
    }

    LaunchedEffect(count) {
        // todo 模拟请求网络
        delay(1000)
        Log.e("TAG", "count = $count")
    }

    Column {
        Button(onClick = { count   }) {
            Text(text = "模拟加载更多")
        }
    }
}

在组合项进入挂载状态时,Log 会打印 count = 0,在触发模拟加载更多后,count 值发生变化,LaunchedEffect 感知到状态发生变更,则会继续触发 网络请求,这时会打印 count = 1,这就是感知组件更新的能力。这里有一点需要注意,如果不停的去点击 count 的话,仅最后一次才会触发 Log,因为每次启动 LaunchedEffect 前,Compose 都会取消上一次还未结束的协程(delay),这也是 LaunchedEffect 启动协程安全的原因

3、模拟 DisposedEffect 感知 组件挂载、组件更新、组件卸载的能力,例如监听好友在线状态能力:

代码语言:javascript复制
@Composable
fun OnlineWidget(vm: OnlineViewModel = viewModel()) {
    // 当前所有用户
    val users = vm.users
    var currentUser by remember { mutableStateOf(users[0])}

    DisposableEffect(key1 = currentUser) {
        vm.registerListener(currentUser)
        Log.e("TAG", "注册 $currentUser 在线状态")
        onDispose {
            vm.unregisterListener(currentUser)
            Log.e("TAG", "反注册 $currentUser 在线状态")
        }
    }

    Column {
        Text(text = "用户 $currentUser 的在线状态是 ${vm.isOnline}")
        LazyColumn() {
            items(users) { user ->
                Button(onClick = {
                    currentUser = user  // 切换用户
                }) {
                    Text(text = "user = $user")
                }
            }
        }
    }
}

DisposableEffect 与 LaunchedEffect 不同,DisposableEffect 的闭包是 DisposableEffectScope,而 LaunchedEffect 的闭包是 CoroutineScope,所以,DisposableEffect 无法像 LaunchedEffect 做一些耗时操作,它更适合去做一些监听与反监听的注册操作,来避免潜在的内存泄漏问题。

DisposableEffect 提供了 onDispose 来感知监听状态的卸载操作,如上在切换用户时,会触发 onDispose 卸载上一次的用户监听,并重新注册新的用户进行监听。如果 OnlineWidget 整个组件在界面上被移除了,onDispose 依然能监听到并触发反注册。

React

React 相比较 Compose 而言会更好理解一点,只需理解 useEffect 即可,他更像是 LaunchedEffect 和 DisposableEffect 的结合,既可以处理耗时操作,也可以感知组件挂载、更新、卸载状态。

1、模拟 useEffect 组件挂载、组件更新、组件卸载的能力,例如如下的定时组件

代码语言:javascript复制
function TimeoutWidget() {
  const [value, setData] = useState(0)
  useEffect(() => {
    const timeout = setTimeout(() => {
      setData(value   1)
    }, 1000)
    return () => clearTimeout(timeout);
  }, [value])
  return <div>
    <h1>{value}</h1>
  </div>
}

组件挂载阶段时,useEffect 初始化 setTimeout 每隔 1s 执行一次,并监听 value 状态的变化,在 1s 结束触发 setData 累加 value 值,这时候,value 只发生变化,将会执行 return 的 clearTimeout 函数,清除定时器,然后重新执行 useEffect 函数继续注册定时监听,在 TimeoutWidget 组件被界面移除时,也会执行 clearTimeout 操作

小结

基于副效应的函数组件,React 和 Compose 都能通过一个函数来替代原来类组件的开发方式,但对于 Compose 来说,仅仅监听组件的 挂载、更新与卸载 往往是不够的,手机端与 PC 端不同,手机端有一些特殊的逻辑需要在息屏与亮屏的时候做一些操作,这是 PC 不会有的场景,所以,对于 React 来说,这三种足够满足业务诉求的开发,对于 Jetpack Compose 来说,官方也考虑到了这种情况,如下是官网监听 onStart、onStop 的示例:

参考资料:

  • 使用 Effect Hook – React[1]
  • Compose 中的附带效应[2]

参考资料

[1]

使用 Effect Hook – React: https://react.docschina.org/docs/hooks-effect.html

[2]

Compose 中的附带效应: https://developer.android.com/jetpack/compose/side-effects?hl=zh-cn#disposableeffect

0 人点赞