React框架的主要功能是专注于View层的渲染,在很长的时间里,对于组件间的通行,状态管理,仅仅使用React自己的能力是不足够的,所以都是通过社区提供的其他库来完成,比如使用mobx,redux生态的各种技术来解决。但是随着React框架不断的进化,新的理念(Hook)、新的Api(新的Context实现)的出现,我觉得完全使用React自带的,去完成状态的管理是完成可行的,所以React即是渲染框架,也是状态管理框架😄。
在过往的经验中,基本上各种方式都使用过,这里谈一些自己的思考。

几种状态管理的简介

比较常见的状态管理方案,在React生态里面其实是非常成熟的了,这里简单的罗列。
redux关联的生态是首当其冲的,通常配合各种中间件来处理副作用,比如redux-saga,redux-thunk等等。其好处是显而易见的,随着项目的复杂程度的上升,其单向数据流、可预测性,使得整个项目更易维护,不在错综复杂,当然其最让人诟病的就是其 action、reducer的写法,即使是一个小功能都需要些非常多的重复代码,让我们很多程序员苦不堪言。平心而论,从理念上来说,这种方式是非常好的,有了统一的方式,我们项目的质量是可控的,易于维护的。
mobx则是通过observable的方式,代理的方式,把状态的更新都封装在对象的getter,setter中,这样我们不再需要向redux一样,写很多的重复的样板代码(action,reducer啦),并不需要显式的定义action,而是可以直接通过比如 ObjectA.x = ‘b’ 的方式直接修改store中数据,从而可以直接出发组件的重绘。
graphql相关生态,把所有的数据都当成是一个大的数据库(本地,后端)的,通过一种query语言通过获取各种数据,或者变更数据。这种方式侵入到了后端,不仅仅是纯粹前端UI的变化,是一整套的数据状态的管理框架。其中facebook自家出的relay库,以及社区的apollo系列库,都是其中的翘楚。

Context + Hook状态管理的例子

Context提供了跨组件交流的通道,Hook提供了能够方便的渲染指令的出发。
这里我们来看看基本的流程是怎么做?下面通过一个简单的计数器的例子来说明

  1. import React, { useState, useMemo, useContext } from 'react'
  2. // 创建一个React Context, 定义context包含的数据结构和默认值
  3. const CounterContext = React.createContext({
  4. count: 0
  5. setCount: () => {}
  6. })
  7. // Provider
  8. const CounterProvider = (props: any) => {
  9. const [count, setCount] = useState<number>(0)
  10. return <CounterContext.Provider value={{ count, setCount }} {...props} />
  11. }
  12. // 计数器组件
  13. const Couter = (props: any) => {
  14. const { count, setCount } = useContext(CounterContext)
  15. const addOne = () => setCount(count++)
  16. return (
  17. <div>
  18. <div>{count}</div>
  19. <button onClick={addOne}>add</button>
  20. </div>
  21. )
  22. }
  23. // 事例App入口
  24. const App = (props: any) => {
  25. return <CounterProvier></CounterProvider>
  26. }

Context可以值传递到组件,hook提供了出发渲染的机制。以上是一个非常简单的例子,但是稍加扩展,就可以用在比较复杂的场景中。
在非常复杂庞大的项目中,纯粹使用Context + Hook的原生方式是不够,必然需要引入其他的方式去处理。其实在绝大部分时候根本没有这么复杂,并不需要使用任何的状态管理的库,或者其他什么框架,随着Hook的出现,使用新的Context Api,完全可以胜任了。
由于没有什么框架支持,那么直接使用,会发现有些地方不是很好的处理,比如如何分解不同的Context,如何处理副作用等等。

分而治之

记得还在上学的时候,上算法课的时候,第一节课老师通过归并排序(merge sort)告诉了我们:分而治之(divide and conquer)可能是这辈子最重要的理念了,要深深牢记。现在工作了多年,想起来,老师说得真是太对了,分而治之的理念在计算机中到处都是。
使用Context + Hook,需要在需要的时候定义Context,把包在该Context的组件管理起来。通过把整个应用,进行分解,不同的部分分成不同的组件,通过不同的Context来管理。
分治是核心理念,简单来说它提醒我们不要愚蠢的把代码写在一个文件一个方法里面。
理念归理念,必须要有一套行之有效的方法论来实践,也就是如何分治?好比有了一套内功,也要有一套招式,把内功外显出来。

一种常见处理方式

边界界定

第一件事,需要做的就是把代码划定一些边界,圈定一些概念,简单来说就是做一些归类,这样才能更好的组织代码。

  1. Context的边界。
    • 整个App的最外层设置一个唯一AppContext,这个Context只放置需要整个App使用的状态数据
    • 每个页面都有一个 PageContext,这个PageContext用来管理本页面下面的状态管理数据。
    • 需要全局管理的小组件,通常是为了完成一件全局统一的事情,比如I18n文案,这种情况下用一个单独的Context管理
  2. 组件职责的界定。
    • 纯组件,这类组件的展示只依赖于传入的props的值,
      • 纯组件只能包含其他纯组件
    • 容器组件,这类组件的展示结果不再只依赖props传入的值,而是会包含:局部或者全局Context的状态值;处理副作用(比如接口调用等);
      • 容器组件可以由其他容器组件 + 纯组件组合而成
      • 也就是App,Page也可以当做是容器组件,而为了方便起见给它们起了特殊的名字

状态和副作用处理的集中思路

定义Context的时候,其实是做两件事情:

  1. 1. 定义状态的数据结构
  2. 2. 改变状态数据值的方法

比如看上面的例子,count是状态数据,setCount就是改变数据的方法,Context中暴露出去的可以是更改状态的方法、获取状态的方法、状态本身。
如何管理状态,一般是以下几种思路

  1. 所有修改状态的逻辑都写在Context中,通过暴露状态修改的方法,状态获取的方法,然后订阅的组件直接去取对应的状态数据和方法,直接调用,这是一种集中式的方法。并且可以配合 useReducer,把同步的状态管理都统一起来,这样可以暴露一个dispatch,有点redux的味道,redux简化版的方式。只要把方法分解好,可以把不同的方法分在不同文件管理,这样一个简化版的redux,也是可以一战的。

    1. const CounterProvider = (props: any) => {
    2. // 比如这里加各种方法来修改状态
    3. const [count, setCount] = useState<number>(0)
    4. const addOne = () => setCount(count + 1)
    5. const getRemoteCount = () => {
    6. // http请求。(这里是例子)
    7. http.get('https://abc.com/a.json').then(res => setCount(res.count))
    8. }
    9. // 也可以用 useReducer。主要处理的是同步的状态修改
    10. const [count, dispatch] = useReducer(function(state, action){
    11. switch(action.type){
    12. case 'increment':
    13. return state + 1
    14. case 'set':
    15. return action.count
    16. }
    17. }, 0)
    18. // 异步
    19. const getRemoteCount = () => {
    20. // http请求。(这里是例子)
    21. http.get('https://abc.com/a.json').then(res => dispatch({ type: 'set', count: res.count))
    22. }
    23. return <CounterContext.Provider value={{ count, addOne, getRemoteCount }} {...props} />
    24. }
  2. Context中不集中管理状态变更副作用,而是通过分治的组件去管理,这就相当于Context值暴露两个东西,一个是状态,一个是状态变更的set方法,或者使用useReducer的话只返回dispatch,只处理简单的同步操作,异步的更改全都在组件里面完成。例子如下: ```jsx // Provider const CounterProvider = (props: any) => { const [counts, setCounts] = useState({ count1, count2 }) return }

// 计数器组件 const Couter = (props: any) => { // 相当于Context值暴露状态的getter和setter // 通过暴露的getter,setter,直接获取状态或者直接变更状态 const { counts, setCounts } = useContext(CounterContext)

// 同步修改 const addOneToCount1 = () => setCount({ …counts, count1: counts.count1 + 1 })

// 异步变更 const addOneToCount1Async = () => setTimeout(() => setCount({ …counts, count1: counts.count1 + 1 }), 5000)

return (

{counts.count1}
) }

  1. 3. 把集中式的和分治的方法结合一下,其实也就是在Context中多提供一些方法而已,大家可以很容易的想象到。
  2. <a name="Qn0zt"></a>
  3. #### 总结
  4. 简单总结这种方法,就是把Context分不同份,然后把状态都放在里面,任何的变动都去操作变更状态数据。不管是使用mobxredux等等,都会不自觉地把所有的状态都放在不管是context中,还是reduxstore中,本质是一样的,只是换了一种工具。<br />**这种思路下面,分治的不是状态,而是改变状态的行为,而状态其实是存在统一的地方。**<br />有了状态管理的各种库、React自带的方法,自然的会把各种组件的状态都放在统一的地方,而组件只要订阅对应的部分状态即可,当状态发生变动,对应订阅的组件也会自动渲染,获取新的状态,然后组件绘制为新的样子。<br />每一个React组件,都是可以自己管理自己的状态的,所以各种状态管理的库,Context也好,都是为了解决**组件间通信的问题,而集中的状态管理是为了方便组件间的通信,通信问题被转换成了状态的管理、存和取的操作,这种方式是极好的,每次状态的变更都是可以追踪。**<br />再举个例子,好比每个组件都是我们使用的微信的一个客户端,每次我们想要和朋友交流,必须是先通过微信的服务器,然后我们才能通信交流。<br />![1.png](https://cdn.nlark.com/yuque/0/2019/png/114891/1562295037184-0392a559-d43d-4f59-ad81-2cc52b62970b.png#align=left&display=inline&height=211&name=1.png&originHeight=211&originWidth=371&size=13139&status=done&width=371)<br />姑且称这种方式为集中式的状态管理方式
  5. <a name="Ud2j2"></a>
  6. ### 换一个角度思考处理方式
  7. 用同样的方法,但是换一个角度去思考,可能形式上相似,但是思考🤔角度就不一样了。<br />我们姑且就称接下来的方法为分布式的状态管理吧(有分布式三个字听起来就很厉害)。先上图:<br />![2 (1).png](https://cdn.nlark.com/yuque/0/2019/png/114891/1562296388864-f1b2b7e2-b6a7-4c18-8dd8-45e68acdea0f.png#align=left&display=inline&height=251&name=2%20%281%29.png&originHeight=251&originWidth=581&size=21223&status=done&width=581)<br />不再把状态交由集中管理,而是每一个组件都管理好自己的状态,以及每个组件需要定义组件自己订阅的通知触发后所需要触发的逻辑。<br />**组件的状态、逻辑都是由组件自己管理,但是组件需要根据一些东西做反应,而这个东西就是组件订阅的通知,一旦订阅的通知触发了,那么组件需要根据通知的内容作出反应,所以有订阅的组件需要对订阅的内容做出反应的逻辑。**<br />我们继续使用Context来实现,这个时候我们在Context中定义的不再是组件的状态,而是通知。(下面只是一个简单的例子来说明这种方式,并不是最优的实现方式,只是简化的概念实现)
  8. ```jsx
  9. // Provider
  10. const CounterProvider = (props: any) => {
  11. const [count, setCount] = useState<number>(0)
  12. // 订阅通知,并返回发送通知的伴随的值
  13. const sub = () => count
  14. // 发送通知,并且可以传一些值
  15. const pub = (count: number) => setCount(count)
  16. return <CounterContext.Provider value={{ sub, pub }} {...props} />
  17. }
  18. // 发送通知
  19. const CountA = () => {
  20. const [count, setCount] = useState<number>(0)
  21. const { pub } = useContext(CounterContext)
  22. const addOne = () => {
  23. setCount(count + 1)
  24. pub(count + 1)
  25. }
  26. return (
  27. <div>
  28. <div>{count}</div>
  29. <button onClick={}>AddOne</button>
  30. </div>
  31. )
  32. }
  33. // 订阅通知,接收通知做出反应
  34. const CountB = () => {
  35. const [count, setCount] = useState<number>(0)
  36. const { sub } = useContext(CounterContext)
  37. const subValue = sub()
  38. useEffect(() => {
  39. // 不管通知什么值,只要通知的值变了就加2
  40. setCount(count + 2)
  41. }, [subValue])
  42. return (
  43. <div>
  44. <div>{count}</div>
  45. </div>
  46. )
  47. }
  48. // 订阅通知,也发送通知
  49. const CountC = () => {
  50. const [count, setCount] = useState<number>(0)
  51. const { sub, pub } = useContext(CounterContext)
  52. const subValue = sub()
  53. useEffect(() => {
  54. // 订阅的通知的值往上加
  55. setCount(count + subValue)
  56. }, [subValue])
  57. // 我不仅订阅,还发送通知
  58. const AddTwo = () => {
  59. setCount(count + 2)
  60. pub(count + 2)
  61. }
  62. return (
  63. <div>
  64. <div onClick={AddTwo}>{count}</div>
  65. </div>
  66. )
  67. }

我们会发现,这不是和定义状态,然后分发状态的写法很像吗?对的,我们使用现有的Context,Redux,Hook的方式可以方便的实现,其外在的物理形式其实是和工具本身所关联的。
形似,但是背后的思路是换了一个了,我们把原本用来做状态管理的,只是看做了消息的通道,而状态的管理全都分权到每个订阅的组件中去了。
我们姑且称这种方式分布式、反应式状态管理吧。

小谈下Hook

Hook的出现彻底改变了对于React组件的整个思路,Functional Component中可以管理状态了,整个API的形式,给代码层面变得更加的简洁。
Dan说:React is not fully Reactive。
当然他说的更多是底层的实现层面的。但是我想说,随着Hook出现,从表层老说,React变得更加的Reactive了,我们可以更加便捷去监听的状态的变更。

结语

一些自己对于状态管理的思考🤔,希望有所帮助吧。