React 状态管理 | 青训营笔记

241 阅读8分钟

1 什么是状态管理

1.1 状态管理简介

解决“嵌套”低于的问题,解决的是跨层级组件之间的数据通信和状态共享。其实随着开发的深入,能不用的情况就不用

1.2 状态管理工具的本质

管理共享内存中的状态

  1. 共享内存
  2. 管理状态
  3. 页面通信
  4. 组建通信
  5. 刷新时效?
    详细定义:单页应用的各个组件本身是共享内存的,如果将状态保存在内存中,就可以读写统一内存中的变量,从而达到状态共享的目的。

1.3 状态管理工具

  • Vue:Vuex(Pinia)
  • Angular:Service和Rxjs
  • React:Flux、Redux、Mobx、Rxjs、Recoil、Jotal、Zustand React和Vue和Angular不一样,React是一个纯UI层的前端架构,UI=fn(state),React将状态的变动完全交给开发者。

2 React状态管理工具简介

2.1 分类

  • React自带:Local State(props)和Context
  • 单向数据流:Flux、Redux(Redux-toolkit)
  • 双向数据绑定:Mobx
  • 原子型状态管理:Recoil、Jotai
  • 异步操作密集型:Rxjs 每一种状态管理工具都有其不同的适用性,不同场景下需要合理的选择状态管理工具。
Context
  • 官方API
  • 渲染粒度无法控制,经常引起不必要的渲染
  • 耦合度高,层层嵌套
Flux
  • 早期经典,已被redux平替
  • dispatch和action耦合,store和action耦合
  • 模板代码最大,不易理解
Redux
  • 单向数据流
  • 纯函数
  • 时间旅行
  • 模板代码复杂,副作用处理
  • 支持类组件和函数组件
Redux-toolkit
  • Redux的hook版
  • 样板代码少
  • 支持React新特性
Mobx
  • 双向绑定,响应式
  • api少,学习成本低,模板代码少
  • 太灵活,很难保证代码风格统一
Recoll
  • 原子性Atom,任意组合selector、细粒度更新(相比于context)
  • 天然支持React Suspense
  • 有快照功能
Jotai
  • 简化版本的Recoll,2.4kb
  • 细粒度更新
  • 不需要Provider包裹,只有两个核心api:atom和useAtom
Zustand
  • 轻量级,不到1kb,适合移动端
  • 只支持hooksz7ujian
Rxjs
  • 异步复杂逻辑处理
  • 多值promise,丰富操作符,时间调度器
  • 异步流程编排
xstate
  • 基于状态机
valtio
  • 基于proxy实现,类似mobx
  • 变更历史和快照

2.1.1 Local State(props)

组件级别的局部状态

import {useState} from 'react'
const Hello = () => {
    const [name,setName] = useState('Jony')
    return <>Hello,{name}</>
}

上述的name就是一个最简单的局部Local State。它只在Hello这个组件中生效,当组件创建时初始化和生效,组件销毁时失效。
React的数据流是自上而下的,大部分情况下Local State可以满足需求。

例外

如果是同parent组件的同级组件之间想传递数据,优先考虑将状态向上一级,放在parent组件中,由parent组件自上而下的传递


当然如果一直这样延伸,会出现一个父组件签到十几层子组件的情况,这个“度”在前端大部分情况下就是子页面。
单页应用中,子页面以及子页面下的组件都是可以用Local State来解决状态管理问题的;而子页面之间,不再需要往上延伸——

子页面间的通信

子页面和子页面间的通信,React本身提供了Context。

2.2 Context

Context解决了React中,props或者state进行多级数据传递,则数据需要自顶下流经过每一级组件,无法跨级的问题。但是Context在页面间共享数据的时候同样有很多问题:

缺点
  1. Context相当于全局变量,难以追溯数据的变更情况
  2. 使用Context的组件内部耦合度太高,不利于组件的复用和单元测试
  3. 会产生不必要的更新(比如会穿透memo和dependicies等)
  4. Context只能存储单一值,无法存储多个各自拥有消费者的值的集合。
  5. 粒度也不太好控制,不能细粒度的区分组件依赖了哪一个Context
  6. 多个Context会存在层层嵌套的问题
解决方法

上述有部分缺点是可以解决的。
其实大部分场景下,不需要三方状态管理工具。Props和Context能解决很多问题。
经常放到Context中的内容:

  • 一些全局的不需要经常变更的配置(主题、语言等)
  • 在不同页面中但相同的属性

2.3 Redux(已淘汰)

Redux由Flux演变而来,利用数据的单向流动的形式对公共状态进行管理。由于已被淘汰,仅参考和借鉴其设计思想。Redux遵循函数式编程的规则。

Redux的前身Flux的状态管理

Flux利用数据的单向流动形式对公共状态进行管理

graph TD
a[Action] --> Dispatcher --> Store --> View
View --> b[Action] --> Dispatcher 

Flux的架构图

  • View:视图层
  • Action:视图发出的消息
  • Dispatcher:派发者,用来接收Action,执行回调函数
  • Store:数据层,存放状态,一旦发生改动,就会更新数据以及emit相关事件等 例子
  1. 在UI页面中触发action
<button onClick = {this.handler.bind(this)}>click</button>
  1. 在FLux的Action中使用dispatcher.dispatch将Action发送给Flux的dispatcher
  2. dispatcher通过register注册事件,然后根据传递过来的action,来改变store中的state
  3. 在store中进行数据更新
  4. 在UI中监听store并触发更新 Flux的缺点
  5. UI组件和容器组件的拆分过于复杂
  6. Action和Dispatcher绑定在一起
  7. 不支持多个store
  8. store被频繁的引入和调用

image (11).png Redux的构架图

Redux如何解决Flux的问题
  • 解耦了action和dispatcher,可以直接通过dispatch发出一个action,不需要注册
  • 解耦了store和dispatcher,提供了Reducer来处理Store的更新
Redux三大原则
  1. 单一数据源,只有一个store,全局state都保存在其中
  2. store中的state是只读的(唯一改变方法:通过action)
  3. 使用纯函数reducer来执行修改
Redux优缺点

优点

  • 适用于大型项目
  • 状态可预测和回溯
  • 可以事故重现
    缺点
  • 相比Context或hooks的useReducer,不适用于中小型项目
  • 需要引入一系列的副作用中间件
  • 需要写过多的样板代码

2.4 Mobx

透明的函数响应式编程。设计类似Vue。使用了可观察对象,可以做到直接修改状态,不用编写actions和reducer image (12).png

优点

  • 上手简单
  • 不用引入各种复杂的中间件
  • 局部精确更新,不用担心粒度控制 缺点
  • 太灵活,代码风格不统一,不方便维护
  • 不能实现时间旅行和回溯
    可优化替代的工具:
  • React hooks
  • useReducer
  • Recoil

2.5 Recoil

官方内置状态管理工具,一定程度上解决了Local State和Context的局限性,且能兼容新特性(Concurrent等)。

解决的问题
  • 组件间的状态共享只能通过将state提升至他们的公共祖先来实现,但这样做可能导致重新渲染一棵巨大组件树
  • Context只能存储单一值,无法存储多个各自拥有消费者的值的集合
核心

Atom原子状态,通过其可以派生出衍生状态Selector

特点
  • 较为官方,提供React新特性的兼容的可能性
  • 细粒度的状态控制
  • 跟Redux实现状态回溯
  • 理解起来没有很复杂,不用写很多样板代码
  • 可以实现状态快照(填充首屏数据、数据状态回滚)

2.6 Zustand

主打轻量级,适合移动端网页开发。
初始化过程中,不仅能保存状态,也能在初始化的时候指定方法和函数。

Zustand核心API和Redux的区别

主要区别:状态的更新。

  • Redux:dispatch和reducer
  • Zustand:setState

3 实现一个简易的状态管理工具

发布/订阅模式
graph TD
Store状态 --发布--> 中介媒介 --发布--> a[React UI层渲染]--订阅--> 中介媒介 --订阅--> Store状态
案例:实现一个简单地Store
export default class CreateStore{
  constructor(reducer,initialState){
      this.currentReducer = reducer;
      this.cuttentState = initialState;
      this.listeners = [];
      this.isDispatching = false;
  }
  
  getState(){
  return this.currentState;
  }
  subscribe(listener){
    this.listeners.push(listener);
    return function unsubscribe(){
      var index = this.listners.indexOf(listener);
      this.listeners.splice(index,1);
    };
  }
  dispatch(action){
    try{
      this.isDispatching = true;
      this.currentState = currentReducer(currentState,action);
    }finally{
      this.isDispatching = false;
    }
    this.listeners.slice().forEach(listener => listener());
    return action;
  }
}

使用这个CreateStore来创建一个全局的状态

import CreateStore from './createStore.js'
function todos(state = [], action){
  switch (action.type){
    case 'ADD_TODO':
      return state.concat([action.text])
    default:
      return state
  }
}
const store = createStore(todos, ['Use Redux'])
store.dispatch({
  type: 'ADD_TODO',
  text: 'Read the docs'
})

console.log(store.getState())

4 Redux在项目中的实践

4.1 如何使用Redux

减少局部状态和redux状态的不合理混用

4.2 Redux复杂的模板代码

  • 同步的、不存在副作用函数:

image (13).png action:原始js对象;reducer:纯函数。

  • 存在副作用函数:

image (14).png 在发出action,到reducer处理函数之间使用中间件处理副作用

中间件的作用

转换异步操作,生成原始action。这样reducer函数就能处理相应的action,从而改变state,更新UI。

4.3 Redux toolkit

解决Redux:样板代码、中间件代码太多,区别同步异步操作复杂的问题。简化开发。


5 个人总结和感想

个人只学过Vue没学过React,有很多知识点都没听说过,不过也有共通点。vue的状态管理基本就用vuex,没想到react会有那么多...