问题思考
- 为什么不能用 window 对象?
- react hooks 给状态管理库的设计带来了哪些新思路?
从全局对象 window 说起
- 全局污染,重复声明?
- 直接取值和赋值,数据变更不清晰?
- 渲染粒度不可控?
- 无法进行时间旅行?
1. 什么是状态管理
从 React 诞生之后,前端组件化的方案深入人心,React 遵循的是单向数据流的原则,属性通过 Props 自上而下的传递。当页面的比较简单,组件之间的层级关系比较浅时,这种自上而下的单向数据流的方式是不会有问题的。如果页面一复杂,组件的嵌套层级一深,这种单向数据流的传递方式,将会使你陷入到“嵌套地狱”。
状态管理本身,解决的就是这种“嵌套”地狱的问题,解决的是跨层级组件之间的数据通信和状态共享。
状态管理工具的本质:管理共享内存中的状态
- 共享内存
- 管理状态
- 页面通信
- 组件通信
- 刷新失效
详细定义:单页应用的各个组件本身是共享内存的,如果将状态保存在内存中,就可以读写统一内存中的变量,从而达到状态共享的目的。
为什么React有这么多状态管理工具?
Vue: Vuex(Pinia)
Angular: Service 和 Rxjs
React: Flux、Redux、Mobx、Rxjs、Recoil、Jotai、Zustand
跟不同前端框架的定义有关,Vue 和 Angular 双向数据绑定,计算属性等,数据是响应式的,控制视图刷新,拥有计算属性等,这些使得 Vue 和 Angular 需要状态管理的场景减少,此外其本身就包含了完整的状态管理工具,比如 Vue 的 Vuex 和 Pinia,Angular 的 Service(RXjs)等,从官方定调。 而 React 不一样,React 是一个纯 UI 层的前端框架,UI=fn(state),React 将状态的变动完全交给开发者。
2. React 状态管理简介
React 状态管理工具
- React自带:Local State(props) 和 Context
- 单向数据流:Flux、Redux(Redux-toolkit)
- 双向数据绑定:Mobx
- 原子型状态管理:Recoil、Jotai
- 异步操作密集型:Rxjs
每一种状态管理工具都有不同的适用性,不同场景下需要合理选择状态管理工具。
2.1 Local State(props)
local State,就是组件级别的局部状态
import {useState} from 'react'
const Hello = () => {
const [name,setName] = useState('Jony')
return <>Hello,{name}</>
}
上述的 name 就是最简单的局部 local State,只在 Hello 组件中生效,当组件创建时初始化和生效,组件销毁时失效。
有个 Parent 父组件,分别有 Child1 和 Child2 两个子组件,它们也分别有自己的 name1 和 name2 。这种情况下,如果我想在 Child1 中获取 Child2 的 name, 或者 Child2 中获取 Child1 的 name,就是这种非父子组件间如何共享状态的问题。
遇到这种问题,我们不需要马上去引入状态管理,对于这种情况我们优先考虑将状态向上一级,放在父组件中,由父组件自上而下的传递。根据这种将状态上升一级的方式,我们如果想在 Child1 中获取 Child2 的 name,只需要:<Child1 name1={name1} name2={name2}>,这样通过 props 的方式,将状态从父组件传递到子组件。
当然这种向上延伸的方法,不是无限的,如果一直往上延伸,会出现一个父组件嵌套 10 几层子组件的情况,必须要有一个“度”,超过这个“度”后,我们就认为 local State 的方式就不太实用了。
这个“度”,在前端开发中,大部分情况下我们认为就是子页面。我们一般认为,单页应用中,子页面以及子页面之下的组件都是可以用 local State 来解决状态管理问题的,而子页面和子页面之间,是不需要再往上延伸。那么子页面和子页面之间如何通信呢?
答案是:子页面和子页面之间的通信,React 本身提供了 Context。
子页面和子页面之间的通信,也可以通过 query 的方式来实现,?a=1&b=2的形式,监听 RL 中的参数就可以进行数据的通信。当然也不是说一定是要子页面和子页面之间,其实跨层级组件的通信都适用。
2.2 Context
OtherDisplay 没有用到 Context 里的 value,但是 Context 的值变换,OtherDisplay 也会重新渲染。
function useCounter() {
let [count, setCount] = useState(0)
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
return { count, decrement, increment }
}
let Counter = createContext(null)
function CounterDisplay() {
let counter = useContext(Counter)
return (
<div>
<button onclick={counter.decrement}>-</button>
<p>You clicked {counter.count} times</p>
<button onclick={counter.decrement}>+</button>
</div>
)
}
function App() {
let counter = useCounter()
return (
<Counter.Provider value={counter}>
<CounterDisplay />
<OtherDisplay />
</Counter.Provider>
)
}
在这个例子中通过 createContext 和 useContext,可以在 App 的子组件 CounterDisplay 中使用 context,从而实现一定意义上的跨组件通信。context 的值一旦变化,所有调用了 useContext() 的组件均会重新触发渲染更新。由于 context API 并不能细粒度地分析某个组件依赖了 context 里的哪个属性,并且它可以穿透 React.memo 和 dependence 的对比,所以针对频繁引起数据变化的场景,在使用时需格外谨慎。
React 中的 Context 解决了 react 中,props 或者 state 进行多级数据传递,则数据需要自顶下流经过每一级组件,无法跨级的问题。但是 Context 在页面间共享数据的时候同样有很多问题:
- Context 相当于全局变量,难以追溯数据的变更情况
- 使用 Context 的组件内部耦合度太高,不利于组件的复用和单元测试
- 会产生不必要的更新(比如会穿透 memo 和 dependicies 等)
- Context 只能存储单一值,无法存储多个各自拥有消费者的值的集合
- 粒度也不太好控制,不能细粒度的区分组件依赖了哪一个 Context
- 多个 Context 会存在层层嵌套的问题
在实际 React 业务代码开发中,要多思考,其实在大部分场景下,我们都不需要三方状态管理工具,Props 和 Context 能解决我们很多问题。
Context 的使用场景很多,一些全局达的不需要经常变更的配置我们经常放到 Context 中,比如主题,语言等:
import React from 'react';
import { ConfigProvider} from '@arco-design/web-react';
import enUS from '@arco-design/web-react/es/locale/en-US';
ReactDOM.render(
<ConfigProvider locale={enUS}>
<YourApp />
</ConfigProvider>,
CONTAINER
);
此外,比如不同的页面中有一些相同的属性,我们也可以放在 Context 中。
2.3 Redux
Redux 是从 Flux 演变而来的,Flux 它是 Facebook 官方给出的应用架构,利用数据的单向流动的形式对公共状态进行管理,不过现在已经被淘汰了,不过其设计思想还是可以参考和借鉴的。
Flux 状态管理架构图:
FLux 利用数据的单向流动的形式对公共状态进行管理。
- View:视图层
- Action:视图发出的消息
- Dispatcher:派发者,用来接收 Action,执行回调函数
- Store:数据层,存放状态,一旦发生改动,就会更新数据以及 emit 相关事件等
例子
-
在 UI 界面中出发 action
<button onClick = {this.handler.bind(this)}>click</button> -
在 Flux 的 Action 中使用
dispatcher.dispatch将 Action 发送给 Flux 的dispatcherhandler() { let action = { type:"add" }; dispatcher.dispatch(action) } -
dispatcher 通过 register 注册事件,然后根据传递过来的 zction,来改变 store 中的 state
const dispatcher = new Dispatcher(); dispatcher.register((action)=>{ switch(action.type){ case "add": store.handleAdd(); break; } }) export default dispatcher; -
在 store 中进行数据更新
import observer from '../observer' let store = Object.assign(observer,{ state:{ n:10 }, getState(){ return this.state; }, handleAdd(){ this.state.n ++; this.$emit("update") }, }) export default dispathcer; -
在 UI 中监听 store 并触发更新
constructor() { super(); this.state = store.getState(); store.$on("update",this.handleUpdate.bind(this)) } handleUpdate(){ this.setState(store.getState()); }
Flux 的缺点
- UI 组件和容器组件的拆分过于复杂
- Action 和 Dispathcer 绑定在一起
- 不支持多个 store
- store 被频繁的引入和调用
Redux 架构图:
Redux 解构了 Action 和 Dispatcher。
Redux 中的 store 也解除了 dispatcher 的耦合,提供了一个 Reducer 来处理 Store 的更新。
Redux 的三大原则:单一数据源,只有一个 store、store 中的 state 是只读的、使用纯函数来执行修改。
-
单一数据源:
在 redux 中,整个应用的全局 State(再次注意是全局 state),都会保存在一个 store 中,一个单一数据源 state tree 也简化了应用的调试和和监控;它也让你在开发中能将应用数据持久化到本地,从而加速开发周期。此外,有一些功能以前很难实现,比如“撤销/重做”,在单一数据源的原则下,使用 Redux 实现将非常容易。
-
Store中的State是只读的:
我们不能直接修改 store 中的 state,store 中的 state 是只读的。唯一能改变 store 中的 state 的方式就是通过 action。
-
使用纯函数来执行修改:
接受纯函数来接受 aciton,该纯函数叫 reducer,可以改变 store 中的 state。
因为 Redux 的上述特性,使得 Redux 可以做时间旅行。时间旅行:就是可以随时穿越到以前和未来,让应用程序切换到任意时间的状态。因此,如果复杂的场景,特别是存在页面组件间复杂的通信的场景非常适合用 Redux 来管理状态。
Redux 比较适合用于大型 Web 项目,尤其是一些交互足够复杂、组件通信频繁的场景,状态可预测和回溯是非常有价值的。还有一种场景,比如需要事故重现,这种定义和上报事故异常和重现的场景,Redux 也很有意义。
Redux 的缺点也很明显,首先为了实现纯函数的 Reducer,Redux 必须处理各种各样的副作用,需要引入一系列的副作用中间件,加重的心智负担,此外 Action,Dispatch,Reducer 的模式需要写过多的样版代码,虽然通过 React hooks 和 Redux toolkit 可以减少一定的样板代码,但是复杂度还是摆在哪里。因此中小项目,也不太推荐使用 Redux,可能 Context 或者 React hooks 中的 useReducer 就能满足需求。
2.4 Mobx
它通过透明的函数响应式编程使得状态管理变得简单和可扩展,Mobx 跟Vue 的设计比较相似,是一个响应式的状态管理库。Mobx 借助于装饰器的实现,使得代码更加简洁易懂。由于使用了可观察对象,所以 Mobx 可以做到直接修改状态,而不必像 Redux 一样编写繁琐的 actions 和 reducers。
步骤
- 页面事件(生命周期、点击事件等等)触发 action 的执行
- 通过 action 来修改状态
- 状态更新后,computed 计算属性也会根据依赖的状态重新计算属性值
- 状态更新后会触发 reaction,从而响应这次状态变化来进行一些操作(渲染组件、打印日志等等)
import { observable, computed } from "mobx";
class OrderLine {
@observable price = 0;
@observable amount = 1;
@computed get total() {
return this.price * this.amount;
}
}
Mobx 的优势在于上手简单,可以直接修改状态,不需要编写繁琐的 Action 和 Reducer,也不需要引入各复杂的中间件,局部精确更新,免去了粒度控制烦恼,自始至终一份引用,不需要 immutable,也没有复制对象的额外开销。因此前端数据流不太复杂的情况,使用 Mobx,因为更加清晰,也便于维护。但是正是因为 MobX 的灵活,MobX 的代码风格很难统一。
不过 Mobx 是不能实现时间旅行和回溯的,因此不太适合前端数据流比较复杂的场景,此外,随着 React hooks,比如 useReducer 等的,以及 React 自身的原子型状态管理工具 Recoil。
2.5 Recoil
Recoil 一定程度上解决了 Local State 和 Context 的局限性,且能够兼容 React 的新特性比如 Concurrent 模式等。
解决的问题
- 组件间的状态共享只能通过将 state 提升至它们的公共祖先来实现,但这样做可能导致重新渲染一颗巨大组件树
- Context 只能存储单一值,无法存储多个各自拥有消费者的值的集合
Recoil 侧是更加的具有原子性,比如在 Recoil 的状态就是 Atom,可以进行任意组合等。
Recoil 的核心,就是 Atom 原子状态,一集通过 Atom 原子状态可以派生出衍生状态 Selector。
// 定义一个 Atom
const textState = atom ({
key: 'textState',
default: '',
});
// 使用 Atom,监听
function TextInput() {
const [text, setText] = useRecoilState(textState);
const onChange = (event) => {
setText(event.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
<br />
Echo: {text}
</div>
);
}
Recoil 主要特点,就是较为官方.提供了与 Concurrent 模式及其他 React 新特性兼容的可能性,主打的是性能。此外因为其原子性的特点,比较容易做到细粒度的状态控制。也能跟 Redux 实现状态回溯,相比较 Redux 而言,还有一个特点就是理解起来没有很复杂,不需要写很多样板代码等。 Recoil 还有一个特点就是可以实现状态快照。比如填充首屏数据和数据状态回滚等。
2.6 Zustand
Zustand 是主打轻量级的状态管理工具,没有 Redux 那样臃肿的设计,也没有兼容 React 类组件的历史包袱,Zustand 状态管理工具体积很小,因此很适合移动端的网页。
Zustand 的使用极其简单,初始化过程中,我们不仅能保存状态,也能在初始化的时候制定方法和函数。
import { create } from 'zustand'
const useBearStore = create((set) =>({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
function BearCounter() {
const bears = useBearStore((state) => state.bears)
return <h1>{bears} around here...</h1>
}
function Controls() {
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button onclick={increasePopulation}>one up</button>
}
Zustand 库的核心 API 和 Redux 极为相似,区别主要在状态的更新,Redux 通过 dispatch 和 reducer 函数来进行状态更新,而 Zustand 则是可以通过 setState 来直接修改状态。
| Redux | Zustand | |
|---|---|---|
| 创建 Store | createStore(reducer, [preloadedState], [enhancer]); | createStore(); |
| 获取 state | getState(); | getState(); |
| 监听 state 的变化 | subscribe(listener); | subscribe(listener); |
| 更新 state | dispatch(action);执行 reducer | setstate(); |
zustand 通过Object.assign函数合并更新状态,同时提供 replace 标志位直接将旧状态完全替换。而 redux 的状态更新则要复杂一些,主要是官方推荐的编程模式将状态更新拆分为多个步骤,dispatch()函数触发一个 Action,而具体处理 Action 以及状态合并的操作均由 Reducer 函数完成,该函数是一个纯函数。这么设计的原因是纯函数对于状态变化来说是可预测的,而且利于测试,更是实现时间旅行类似功能的基础。
3. 实现一个简易的状态管理工具
发布/订阅模式
实现一个简单的 Store
export default class Createstore{
constructor(reducer, initialstate){
this.currentReducer = reducer;
this.currentState = initialstate;
this.listeners =[];
this.isDispatching= false;
}
getstate() {
return this.currentState;
}
subscribe(listener) {
this.listeners.push(listener);
return function unsubscribe( ) {
var index = this.listeners.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;
}
}
我们也可以通过 Store.subscribe 来监听状态变化,重新渲染 React UI 层等。
4. Redux 在项目中的实战
4.1 如何使用 Redux
redux 的本质 redux 做为一款状态管理工具,主要是为了解次组件问通信的问题。
既然是组件间的通信问题,那么显然将所有页面的状态都放入 redux 中,是不合理的,复杂度也很高。
减少局部状态和 redux 状态的不合理混用
全量使用 redux 的复杂度很高,我们当然考虑将一部分状态放在 redux 中,一部分状态放在 local state 中,但是这种情况下,很容易产生一个问题,就是如果 local State 跟 redux 中的 state 存在状态依赖。
4.2 Redux 复杂的模板代码
redux 是遵循函数式编程的规则,上述的数据流中,action 是一个原始 js 对象(plain object)且 reducer 是一个纯函数,对于同步且没有副作用的操作,上述的数据流起到可以管理数据,从而控制视图层更新的目的。
如果存在副作用函数,那么我们需要首先处理副作用函数,然后生成原始的 js 对象。如何处理副作用操作,在 redux 中选择在发出 action,到 reducer 处理函数之间使用中间件处理副作用。
在有副作用的 action 和原始的 action 之间增加中间件处理,从图中我们也可以看出,中间件的作用就是:
转换异步操作,生成原始的 action,这样,reducer 函数就能处理相应的 action,从而改变 state,更新Ul。
因为中间件,纯函数 Reducer 等使得 Redux 需要写很多样板代码,使用起来越来越复杂,早期我们使用 redux-thunk,或者 redux-saga 等,但是复杂度还是在那里,因此在项目中不推荐使用如此复杂的 Redux 以及相关逻辑。
4.3 Redux toolkit
用 redux 需要有太多的样板代码,中间件代码等等,还需要区别同步和异步操作,及其复杂。早期 Dvajs 通过封装,能够解决部分上述的问题。早期的 Redux,我们也需要引入很多 Redux 相关的包,比如 React-redux 等,显得复杂而繁琐,而 Redux toolkit 的出现则是完全解决了上述的问题,使得 Redux 的开发可以简单明了。Redux toolkit 是官方推荐的高校的 redux 状态管理工具集。
Redux tookit 可以简化 Redux 开发,包括配置 store、定义 reducer,不可变的更新逻辑、甚至可以立即创建整个状态的“切片 slice" ,而无需手动编写任何 action creator 或者 action type。此外,Redux toolkit 提供了完整的 React 的 hooks,可以方便 React 函数组件中使用 Redux toolkit。