1、React 状态、通信
React 为什么需要状态管理
React 特点:
- 专注 view 层:专注 view 层 的特点决定了它不是一个全能框架,相比 angular 这种全能框架,React 功能较简单,单一。
- UI=render(data)UI=render(data),data就是我们说的数据流,render是react提供的纯函数,所以用户界面的展示完全取决于数据层。
- 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为子组件添加属性数据,即可实现父组件向子组件通信
实现步骤
-
父组件提供要传递的数据 -
state
-
给子组件标签
添加属性
值为 state中的数据 -
子组件中通过
props
接收父组件中传过来的数据 -
- 类组件使用this.props获取props对象
- 函数式组件直接通过参数获取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说明:
- props是只读对象(readonly)根据单项数据流的要求,子组件只能读取props中的数据,不能进行修改
- props可以传递任意数据数字、字符串、布尔值、数组、对象、函数、JSX
子传父:父组件给子组件传递回调函数,子组件调用
实现步骤
- 父组件提供一个回调函数 - 用于接收数据
- 将函数作为属性的值,传给子组件
- 子组件通过props调用 回调函数
- 将子组件中的数据作为参数传递给回调函数
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
兄弟组件通信
兄弟组件:通过状态提升机制,利用共同的父组件实现兄弟通信
状态提升
状态提升:即把需要通信的 state 提升到两者共同的父组件,实现共享和 Reaction
实现内容:A,B 组件下的 A1,B1 要实现 state 通信
状态提升流程:在 A,B 之上增加一个 Container 组件,并把 A1,B1 需要共享的状态提升,定义到 Container,通过 props 传递 state 以及 changeState 的方法。
此方式存在的一个问题是:以后如果有一个 C 组件的 state,与 A 要做通信,就会再添加一个 Container 组件,如果是 A 的 state 要跟 C 共享,更是毁灭性打击,之前提升到 Container 的 state,还要再提升一层。这种无休止的状态提升问题,后期的通信成本非常高,几乎是重写。
实现步骤
- 将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态
-
- 提供共享状态
- 提供操作共享状态的方法
- 要接收数据状态的子组件通过 props 接收数据
- 要传递数据状态的子组件通过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处理。
Redux
Redux是进化Flux,它是在Flux架构模式指导下生成的代码框架,也进一步进行架构约束设计
为什么要使用Redux?
- 独立于组件,无视组件之间的层级关系,简化通信问题
- 单项数据流清晰,易于定位bug
- 调试工具配套良好,方便调试
三个原则:
1、数据来源的唯一性:在redux中所有的数据都是存放在store中,store是单一不可变状态树,这样做的目的就是保证数据来源的唯一性。单个状态树也使调试或检查应用程序变得更容易。
2、state只能是只读的状态: state只能是只读的,State 只能通过触发 Action 来更改,在action中可以去取值,但是不能够去改变它,这个时候采取的方式通常是深度拷贝state,并且将其返回给一个变量,然后改变这个变量,最后将值返回出去。而且要去改变数据只能够在的reducer中,reducer是一个描述了对象发生了一个什么样过程的函数过程。 只读状态的好处,确保视图和网络回调都不会直接写入状态。
3、使用纯函数进行改变:reducer的实质其实就是一个纯函数。每次更改总是返回一个新的 State
Redux数据流架构
- store:提供了一个全局的store变量,用来存储我们希望从组件内部抽离出去的那些公用的状态;
- action:提供了一个普通对象,用来描述你想怎么改数据,并且这是唯一的途径;
- reducer:提供了一个纯函数,根据action的描述更新state
纯Redux实现计数器
核心步骤
- 创建reducer函数 在内部定义好action和state的定义关系
- 调用Redux的createStore方法传入定义好的reducer函数生成store实例
- 通过store实例身上的subscribe方法监控数据是否变化
- 点击按钮 通过专门的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数据流
响应式数据. 首先使用@observable
将数据转换为‘响应式数据’,类似于 Vue 的 data。这些数据在一些上下文(例如 computed,observer 的包装的 React 组件,reaction)中被访问时可以被收集依赖,当这些数据变动时相关的依赖就会被通知. 响应式数据带来的两个优点是 ① 简化数据操作方式(相比 redux 和 setState); ② 精确的数据绑定,只有数据真正变动时,视图才需要渲染,组件依赖的粒度越小,视图就可以更精细地更新
核心:
动作改变状态,状态的改变会更新所有受影响的视图
基础使用
初始化mobx
初始化步骤
- 定义数据状态state
- 在构造器中实现数据响应式处理 makeAutoObservble
- 定义修改数据的函数action
- 实例化store并导出
import { makeAutoObservable } from 'mobx'
class CounterStore {
count = 0 // 定义数据
constructor() {
makeAutoObservable(this) // 响应式处理
}
// 定义修改数据的方法
addCount = () => {
this.count++
}
}
const counter = new CounterStore()
export default counter
React使用store
实现步骤
- 在组件中导入counterStore实例对象
- 在组件中使用storeStore实例对象中的数据
- 通过事件调用修改数据的方法修改store中的数据
- 让组件响应数据变化
// 导入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)
计算属性(衍生状态)
有一些状态根据现有的状态计算(衍生)得到
实现步骤
- 生命一个存在的数据
- 通过get关键词 定义计算属性
- 在 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)
异步数据处理
实现步骤:
- 在mobx中编写异步请求方法 获取数据 存入state中
- 组件中通过 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)
模块化
实现步骤
- 拆分模块js文件,每个模块中定义自己独立的state/action
- 在store/index.js中导入拆分之后的模块,进行模块组合
- 利用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 等工具不是好的选择。