对 React 状态管理的理解及方案对比

1、React 状态、通信
React 为什么需要状态管理
React 特点:
  1. 专注 view 层:专注 view 层 的特点决定了它不是一个全能框架,相比 angular 这种全能框架,React 功能较简单,单一。
  2. UI=render(data)UI=render(data),data就是我们说的数据流,render是react提供的纯函数,所以用户界面的展示完全取决于数据层。
  3. state 自上而下流向、Props 只读React是根据state(或者props)去渲染页面的,state 流向是自组件从外到内,从上到下的,而且传递下来的 props 是只读的。
组件内状态管理
React state

通过 state 管理状态,使用 setState 更新状态,从而更新UI

export default class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: 0,
      otherValue: 0
    };
  }

  handleAdd() {
    this.setState((state, props) => ({
    ...state,
      value: ++state.value
    }));
  }

  render() {
    return (
      <div>
        <p>{this.state.value}</p>
        <button onClick={() => this.handleAdd}>Add</button>
      </div>
    );
  }
}

react自身的状态管理方案缺点:即使只更新state中某个值,也需要带上其他值(otherValue),操作繁琐

React Hooks

React16.8中正式增加了hooks。通过hooks管理组件内状态,简单易用可扩展。

动机:

  • 在组件之间复用状态逻辑很难
  • 复杂组件变得难以理解
  • 难以理解的 class
  • React Hooks 的设计目的,就是加强版函数组件,完全不使用"类"来定义组件,就能写出一个全功能的组件。
import React, { useState } from "react";

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

export default Counter;

对比 Class 组件中,使用react自身的状态管理方法,React Hooks使用useState方法在 Function 组件中创建状态变量、更新状态的方法、赋值初始状态。这样就实现了一个拥有自己的状态的 Function 组件。

React 组件通信

react自身的数据流管理方案有三种:

  • 父子组件通信
  • 兄弟组件通信
  • 跨组件通信
父子组件

父传子:通过props为子组件添加属性数据,即可实现父组件向子组件通信

对 React 状态管理的理解及方案对比_第1张图片

实现步骤

  1. 父组件提供要传递的数据 - state

  2. 给子组件标签添加属性值为 state中的数据

  3. 子组件中通过 props 接收父组件中传过来的数据

    1. 类组件使用this.props获取props对象
    2. 函数式组件直接通过参数获取props对象
import React from 'react'

// 函数式子组件
function FSon(props) {
  console.log(props)
  return (
    <div>
      子组件1
      {props.msg}
    </div>
  )
}

// 类子组件
class CSon extends React.Component {
  render() {
    return (
      <div>
        子组件2
        {this.props.msg}
      </div>
    )
  }
}
// 父组件
class App extends React.Component {
  state = {
    message: 'this is message'
  }
  render() {
    return (
      <div>
        <div>父组件</div>
        <FSon msg={this.state.message} />
        <CSon msg={this.state.message} />
      </div>
    )
  }
}

export default App

props说明:

  1. props是只读对象(readonly)根据单项数据流的要求,子组件只能读取props中的数据,不能进行修改
  2. props可以传递任意数据数字、字符串、布尔值、数组、对象、函数、JSX

子传父:父组件给子组件传递回调函数,子组件调用

实现步骤

  1. 父组件提供一个回调函数 - 用于接收数据
  2. 将函数作为属性的值,传给子组件
  3. 子组件通过props调用 回调函数
  4. 将子组件中的数据作为参数传递给回调函数

对 React 状态管理的理解及方案对比_第2张图片

import React from 'react'

// 子组件
function Son(props) {
  function handleClick() {
    // 调用父组件传递过来的回调函数 并注入参数
    props.changeMsg('this is newMessage')
  }
  return (
    <div>
      {props.msg}
      <button onClick={handleClick}>change</button>
    </div>
  )
}


class App extends React.Component {
  state = {
    message: 'this is message'
  }
  // 提供回调函数
  changeMessage = (newMsg) => {
    console.log('子组件传过来的数据:',newMsg)
    this.setState({
      message: newMsg
    })
  }
  render() {
    return (
      <div>
        <div>父组件</div>
        <Son
          msg={this.state.message}
          // 传递给子组件
          changeMsg={this.changeMessage}
        />
      </div>
    )
  }
}

export default App
兄弟组件通信

兄弟组件:通过状态提升机制,利用共同的父组件实现兄弟通信

对 React 状态管理的理解及方案对比_第3张图片

状态提升

状态提升:即把需要通信的 state 提升到两者共同的父组件,实现共享和 Reaction

对 React 状态管理的理解及方案对比_第4张图片

实现内容:A,B 组件下的 A1,B1 要实现 state 通信

对 React 状态管理的理解及方案对比_第5张图片

状态提升流程:在 A,B 之上增加一个 Container 组件,并把 A1,B1 需要共享的状态提升,定义到 Container,通过 props 传递 state 以及 changeState 的方法。

此方式存在的一个问题是:以后如果有一个 C 组件的 state,与 A 要做通信,就会再添加一个 Container 组件,如果是 A 的 state 要跟 C 共享,更是毁灭性打击,之前提升到 Container 的 state,还要再提升一层。这种无休止的状态提升问题,后期的通信成本非常高,几乎是重写。

实现步骤

  1. 将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态
    • 提供共享状态
    • 提供操作共享状态的方法
  1. 要接收数据状态的子组件通过 props 接收数据
  2. 要传递数据状态的子组件通过props接收方法,调用方法传递数据
import React from 'react'

// 子组件A
function SonA(props) {
  return (
    <div>
      SonA
      {props.msg}
    </div>
  )
}
// 子组件B
function SonB(props) {
  return (
    <div>
      SonB
      <button onClick={() => props.changeMsg('new message')}>changeMsg</button>
    </div>
  )
}

// 父组件
class App extends React.Component {
  // 父组件提供状态数据
  state = {
    message: 'this is message'
  }
  // 父组件提供修改数据的方法
  changeMsg = (newMsg) => {
    this.setState({
      message: newMsg
    })
  }

  render() {
    return (
      <>
        {/* 接收数据的组件 */}
        <SonA msg={this.state.message} />
        {/* 修改数据的组件 */}
        <SonB changeMsg={this.changeMsg} />
      </>
    )
  }
}

export default App
跨组件Context:

跨组件Context:Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法

实现步骤

1- 创建Context对象 导出 Provider 和 Consumer对象

const { Provider, Consumer } = createContext()

2- 使用Provider包裹上层组件提供数据

<Provider value={this.state.message}>
    {/* 根组件 */}
</Provider>

3- 需要用到数据的组件使用Consumer包裹获取数据

<Consumer >
    {value => /* 基于 context 值进行渲染*/}
</Consumer>

完整代码

import React, { createContext }  from 'react'

// 1. 创建Context对象 
const { Provider, Consumer } = createContext()


// 3. 消费数据
function ComC() {
  return (
    <Consumer >
      {value => <div>{value}</div>}
    </Consumer>
  )
}

function ComA() {
  return (
    <ComC/>
  )
}

// 2. 提供数据
class App extends React.Component {
  state = {
    message: 'this is message'
  }
  render() {
    return (
      <Provider value={this.state.message}>
        <div className="app">
          <ComA />
        </div>
      </Provider>
    )
  }
}

export default App

问题:

  • context只是将状态提升至共有的父组件来实现,看似跨组件,实则还是逐级传递来实现组件间通信、状态同步以及状态共享
  • context也是将底部子组件的状态控制交给到了顶级组件,但是顶级组件状态更新的时候一定会触发所有子组件的re-render,那么也会带来损耗。
跨组件通信会产生的问题:
  • 组件臃肿:当组件的业务逻辑非常复杂时,我们会发现代码越写越多,因为我们只能在组件内部去控制数据流,没办法抽离,Model和View都放在了View层,整个组件显得臃肿不堪,难以维护。
  • 状态不可预知,甚至不可回溯: 当数据流混乱时,一个执行动作可能会触发一系列的setState,整个数据流变得不可“监控”
  • 处理异步数据流:react自身并未提供多种处理异步数据流管理的方案,仅用一个setState已经很难满足一些复杂的异步流场景
  • 状态无休止提升,context 不好用
状态管理定义:

简单来说来说,“状态管理” 就是为了解决组件间的 “跨级” 通信。

  • 一个组件需要和另一个组件共享状态
  • 一个组件需要改变另一个组件的状态
2、Flux —> Redux —> Mobx
Flux架构模式的诞生

Flux是一套架构模式,而不是代码框架。

【单向数据流】:Action -> Dispatcher -> Store -> View页面交互数据流,如用户点击按钮:View -> Create Action -> Dispatcher(由此进入【单向数据流】)

四大核心部分

  • dispatcher:负责两件事,一是分发和处理Action,二是维护Store。
  • Store:数据和逻辑部分。
  • views:页面-React 组件,从 Store 获取状态(数据),绑定事件处理。
  • actions:交互封装为action,提交给Store处理。

对 React 状态管理的理解及方案对比_第6张图片

Redux

Redux是进化Flux,它是在Flux架构模式指导下生成的代码框架,也进一步进行架构约束设计

为什么要使用Redux?
  1. 独立于组件,无视组件之间的层级关系,简化通信问题
  2. 单项数据流清晰,易于定位bug
  3. 调试工具配套良好,方便调试
三个原则:

1、数据来源的唯一性:在redux中所有的数据都是存放在store中,store是单一不可变状态树,这样做的目的就是保证数据来源的唯一性。单个状态树也使调试或检查应用程序变得更容易。

2、state只能是只读的状态: state只能是只读的,State 只能通过触发 Action 来更改,在action中可以去取值,但是不能够去改变它,这个时候采取的方式通常是深度拷贝state,并且将其返回给一个变量,然后改变这个变量,最后将值返回出去。而且要去改变数据只能够在的reducer中,reducer是一个描述了对象发生了一个什么样过程的函数过程。 只读状态的好处,确保视图和网络回调都不会直接写入状态。

3、使用纯函数进行改变:reducer的实质其实就是一个纯函数。每次更改总是返回一个新的 State

Redux数据流架构

对 React 状态管理的理解及方案对比_第7张图片

  • store:提供了一个全局的store变量,用来存储我们希望从组件内部抽离出去的那些公用的状态;
  • action:提供了一个普通对象,用来描述你想怎么改数据,并且这是唯一的途径;
  • reducer:提供了一个纯函数,根据action的描述更新state
纯Redux实现计数器

核心步骤

  1. 创建reducer函数 在内部定义好action和state的定义关系
  2. 调用Redux的createStore方法传入定义好的reducer函数生成store实例
  3. 通过store实例身上的subscribe方法监控数据是否变化
  4. 点击按钮 通过专门的dispatch函数 提交action对象 实现数据更新
<button id="decrement">-</button>
<span id="count">0</span>
<button id="increment">+</button>

<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>

<script>
  // 定义reducer函数 
  // 内部主要的工作是根据不同的action 返回不同的state
  function counterReducer (state = { count: 0 }, action) {
    switch (action.type) {
      case 'INCREMENT':
        return { count: state.count + 1 }
      case 'DECREMENT':
        return { count: state.count - 1 }
      default:
        return state
    }
  }
  // 使用reducer函数生成store实例
  const store = Redux.createStore(counterReducer)
  
  // 订阅数据变化
  store.subscribe(() => {
    console.log(store.getState())
    document.getElementById('count').innerText = store.getState().count
    
  })
  // 增
  const inBtn = document.getElementById('increment')
  inBtn.addEventListener('click', () => {
    store.dispatch({
      type: 'INCREMENT'
    })
  })
  // 减
  const dBtn = document.getElementById('decrement')
  dBtn.addEventListener('click', () => {
    store.dispatch({
      type: 'DECREMENT'
    })
  })
</script>
Redux异步处理
import { createSlice } from '@reduxjs/toolkit'
import axios from 'axios'

const channelStore = createSlice({
  name: 'channel',
  initialState: {
    channelList: []
  },
  reducers: {
    setChannelList (state, action) {
      state.channelList = action.payload
    }
  }
})


// 创建异步
const { setChannelList } = channelStore.actions
const url = 'http://geek.itheima.net/v1_0/channels'
// 封装一个函数 在函数中return一个新函数 在新函数中封装异步
// 得到数据之后通过dispatch函数 触发修改
const fetchChannelList = () => {
  return async (dispatch) => {
    const res = await axios.get(url)
    dispatch(setChannelList(res.data.data.channels))
  }
}

export { fetchChannelList }

const channelReducer = channelStore.reducer
export default channelReducer
import { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { fetchChannelList } from './store/channelStore'

function App () {
  // 使用数据
  const { channelList } = useSelector(state => state.channel)
  useEffect(() => {
    dispatch(fetchChannelList())
  }, [dispatch])

  return (
    <div className="App">
      <ul>
        {channelList.map(task => <li key={task.id}>{task.name}</li>)}
      </ul>
    </div>
  )
}

export default App
什么时候应该使用 Redux?

当需要处理复杂的应用状态,且 React 本身无法满足时. 比如:

  • 需要持久化应用状态, 这样可以从本地存储或服务器返回数据中恢复应用
  • 需要实现撤销重做这些功能
  • 实现跨页面的用户协作
  • 应用状态很复杂时
  • 数据流比较复杂时
  • 许多不相关的组件需要共享和更新状态
  • 外置状态
redux的缺点:
  • 繁重的代码模板:修改一个state可能要动四五个文件;
  • store里状态残留:多组件共用store里某个状态时要注意初始化清空问题;
  • 无脑的发布订阅:每次dispatch一个action都会遍历所有的reducer,重新计算connect,这无疑是一种损耗;
  • 交互频繁时会有卡顿:如果store较大时,且频繁地修改store,会明显看到页面卡顿;
  • 不支持typescript;
Mobx

一个可以和React良好配合的集中状态管理工具,和Redux解决的问题相似,都可以独立组件进行集中状态管理

Mobx 提供了一个类似 Vue 的响应式系统,相对 Redux 来说 Mobx 的架构更容易理解。

Mobx数据流

对 React 状态管理的理解及方案对比_第8张图片

响应式数据. 首先使用@observable 将数据转换为‘响应式数据’,类似于 Vue 的 data。这些数据在一些上下文(例如 computed,observer 的包装的 React 组件,reaction)中被访问时可以被收集依赖,当这些数据变动时相关的依赖就会被通知. 响应式数据带来的两个优点是 ① 简化数据操作方式(相比 redux 和 setState); ② 精确的数据绑定,只有数据真正变动时,视图才需要渲染,组件依赖的粒度越小,视图就可以更精细地更新

核心:

动作改变状态,状态的改变会更新所有受影响的视图

基础使用

初始化mobx

初始化步骤

  1. 定义数据状态state
  2. 在构造器中实现数据响应式处理 makeAutoObservble
  3. 定义修改数据的函数action
  4. 实例化store并导出
import { makeAutoObservable } from 'mobx'

class CounterStore {
  count = 0 // 定义数据
  constructor() {
    makeAutoObservable(this)  // 响应式处理
  }
  // 定义修改数据的方法
  addCount = () => {
    this.count++
  }
}

const counter = new CounterStore()
export default counter

React使用store

实现步骤

  1. 在组件中导入counterStore实例对象
  2. 在组件中使用storeStore实例对象中的数据
  3. 通过事件调用修改数据的方法修改store中的数据
  4. 让组件响应数据变化
// 导入counterStore
import counterStore from './store'
// 导入observer方法
import { observer } from 'mobx-react-lite'
function App() {
  return (
    <div className="App">
      <button onClick={() => counterStore.addCount()}>
        {counterStore.count}
      </button>
    </div>
  )
}
// 包裹组件让视图响应数据变化
export default observer(App)

计算属性(衍生状态)

有一些状态根据现有的状态计算(衍生)得到

对 React 状态管理的理解及方案对比_第9张图片

实现步骤

  1. 生命一个存在的数据
  2. 通过get关键词 定义计算属性
  3. 在 makeAutoObservable 方法中标记计算属性
import { computed, makeAutoObservable } from 'mobx'

class CounterStore {
  list = [1, 2, 3, 4, 5, 6]
  constructor() {
    makeAutoObservable(this, {
      filterList: computed
    })
  }
  // 修改原数组
  changeList = () => {
    this.list.push(7, 8, 9)
  }
  // 定义计算属性
  get filterList () {
    return this.list.filter(item => item > 4)
  }
}

const counter = new CounterStore()

export default counter
// 导入counterStore
import counterStore from './store'
// 导入observer方法
import { observer } from 'mobx-react-lite'
function App() {
  return (
    <div className="App">
      {/* 原数组 */}
      {JSON.stringify(counterStore.list)}
      {/* 计算属性 */}
      {JSON.stringify(counterStore.filterList)}
      <button onClick={() => counterStore.changeList()}>change list</button>
    </div>
  )
}
// 包裹组件让视图响应数据变化
export default observer(App)

异步数据处理

实现步骤:

  1. 在mobx中编写异步请求方法 获取数据 存入state中
  2. 组件中通过 useEffect + 空依赖 触发action函数的执行
// 异步的获取

import { makeAutoObservable } from 'mobx'
import axios from 'axios'

class ChannelStore {
  channelList = []
  constructor() {
    makeAutoObservable(this)
  }
  // 只要调用这个方法 就可以从后端拿到数据并且存入channelList
  setChannelList = async () => {
    const res = await axios.get('http://geek.itheima.net/v1_0/channels')
    this.channelList = res.data.data.channels
  }
}
const channlStore = new ChannelStore()
export default channlStore
import { useEffect } from 'react'
import { useStore } from './store'
import { observer } from 'mobx-react-lite'
function App() {
  const { channlStore } = useStore()
  // 1. 使用数据渲染组件
  // 2. 触发action函数发送异步请求
  useEffect(() => {
    channlStore.setChannelList()
  }, [])
  return (
    <ul>
      {channlStore.channelList.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
}
// 让组件可以响应数据的变化[也就是数据一变组件重新渲染]
export default observer(App)

模块化

对 React 状态管理的理解及方案对比_第10张图片

实现步骤

  1. 拆分模块js文件,每个模块中定义自己独立的state/action
  2. 在store/index.js中导入拆分之后的模块,进行模块组合
  3. 利用React的context的机制导出统一的useStore方法,给业务组件使用

1- 定义task模块

import { makeAutoObservable } from 'mobx'

class TaskStore {
  taskList = []
  constructor() {
    makeAutoObservable(this)
  }
  addTask () {
    this.taskList.push('vue', 'react')
  }
}

const task = new TaskStore()


export default task

2- 定义counterStore

import { makeAutoObservable } from 'mobx'

class CounterStore {
  count = 0
  list = [1, 2, 3, 4, 5, 6]
  constructor() {
    makeAutoObservable(this)
  }
  addCount = () => {
    this.count++
  }
  changeList = () => {
    this.list.push(7, 8, 9)
  }
  get filterList () {
    return this.list.filter(item => item > 4)
  }
}

const counter = new CounterStore()

export default counter

3- 组合模块导出统一方法

import React from 'react'

import counter from './counterStore'
import task from './taskStore'


class RootStore {
  constructor() {
    this.counterStore = counter
    this.taskStore = task
  }
}


const rootStore = new RootStore()

// context机制的数据查找链  Provider如果找不到 就找createContext方法执行时传入的参数
const context = React.createContext(rootStore)

const useStore = () => React.useContext(context)
// useStore() =>  rootStore  { counterStore, taskStore }

export { useStore }

4- 组件使用模块中的数据

import { observer } from 'mobx-react-lite'
// 导入方法
import { useStore } from './store'
function App() {
  // 得到store
  const store = useStore()
  return (
    <div className="App">
      <button onClick={() => store.counterStore.addCount()}>
        {store.counterStore.count}
      </button>
    </div>
  )
}
// 包裹组件让视图响应数据变化
export default observer(App)
mobx的缺陷
  • 没有状态回溯能力:mobx是直接修改对象引用,所以很难去做状态回溯;(这点redux的优势就瞬间体现出来了)
  • 没有中间件:和redux一样,mobx也没有很好地办法处理异步数据流,没办法更精细地去控制数据流动
  • store太多:随着store数的增多,维护成本也会增加,而且多store之间的数据共享以及相互引用也会容易出错
  • 副作用:mobx直接修改数据,和函数式编程模式强调的纯函数相反,这也导致了数据的很多未知性
Mobx和Redux的对比

redux

  • redux将数据保存在单一的store中
  • redux使用plain object保存数据,需要手动处理变化后的操作
  • redux使用的是不可变状态,意味着状态只是只读的,不能直接去修改它,而是应该返回一个新的状态,同时使用纯函数
  • redux会比较复杂,因为其中的函数式编程思想掌握起来不是那么容易,同时需要借助一系列的中间件来处理异步和副作用
  • 数据流:dispatch(action) - > 在store中调用reducer,将当前state和收到的action做对比,计算出新的state -> state发生变化,store监听到变化 -> 出发重新渲染

mobx

  • mobx将数据保存在多个独立的store中,按模块应用划分
  • mobx使用observable保存数据,数据变化后自动处理响应的操作。
  • mobx中的状态是可变的,可以直接对其进行修改
  • mobx相对来说比较简单,在其中有很多的抽象,mobx使用的更多的是面向对象的思维
  • 数据流:action -> 修改state -> 触发变化

总结

  • 如果是小型的项目且没有多少状态需要共享,那么不需要状态管理,react 本身的 props 或者 context 就能实现需求
  • 如果需要手动控制状态的更新,单向数据流是合适的选择,例如:redux
  • 如果需要简单的自动更新,双向绑定的状态管理是不二之选,例如:mobx
  • 在小型项目或者少量开发人员的项目中,可以采用 MobX,效率会更高一点。
  • 大型项目或者多人协助的项目,考虑采用 Redux,后续维护成本更低。
  • 如果是两个或多个组件之间简单的数据共享,那么原子化或许是合适的选择:,例如:jotai,recoil
  • 如果状态有复杂数据流的处理,请用 rxjs
  • 如果管理的是复杂的业务状态,那么可以使用有限状态机做状态的跳转管理,例如:xstate
  • 如果有在非react上下文订阅、操作状态的需求,那么 jotai、recoil 等工具不是好的选择。

你可能感兴趣的:(react.js,javascript,前端)