redux
redux 是 React 社区目前最流行的状态管理方案。
redux 的数据流
redux 是单向数据流。组件通过 dispatch 一个 action 来触发 store 的修改,然后由 store 的 reducer 函数计算出新的状态,由于组件监听了 store 的数据变化,组件会拿到最新的数据重新渲染。这种数据流动是单向的,特别清晰。
Redux 三个基本原则:
- 单一数据源。所有的状态都放到一个 store 里面,一个应用中一般只有一个 store
- 保持状态只读。在 Redux 中,只能通过 dispatch 一个 action 来修改 state
- 修改数据只能通过纯函数完成。
redux 基础
action: 是一个用于描述修改事件的普通对象。
reducer:规范 state 创建流程的函数。处理 action 的纯函数,通过传入 action 对象以及旧的 state,返回新的 state。
dispatch: 规范 setState 流程的函数。通过 dispatch 一个 action 来触发 store 的修改
连接 React (react-redux)
- 使用 Provider 在根组件 注入 Store
- 组件使用 connect() 方法连接 Redux
中间件
中间件(middleware)就是一个对 store 的 dispatch 进行修饰的函数。applyMiddleware 就是对 store.dispatch 做了层层包装,最后返回修改了 dispatch 之后的 store。
function applyMiddleware(middlewares) {
let dispatch = store.dispatch;
middlewares.forEach((middleware) => (dispatch = middleware(store)(dispatch)));
return { ...store, dispatch };
}
redux-thunk 中间件就是加了判断 action 是否是一个函数的逻辑
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) =>
(next) =>
(action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
redux-promise 则是判断 action.payload 是否是个 promise
一个简单实现
import React, { useState, useContext, useEffect } from 'react';
const appContext = React.createContext(null);
let state = undefined;
let reducer = undefined;
const listeners = [];
const setState = (newState) => {
// console.log(newState);
state = newState;
listeners.map((fn) => fn(state));
};
const store = {
getState() {
return state;
},
dispatch: (action) => {
setState(reducer(state, action));
},
subscribe(fn) {
listeners.push(fn);
return () => {
const index = listeners.findIndex(fn);
listeners.splice(index, 1);
};
},
};
let dispatch = store.dispatch;
const prevDispatch = dispatch;
// redux-thunk
dispatch = (action) => {
if (typeof action === 'function') {
action(dispatch);
} else {
prevDispatch(action);
}
};
const preDispatch2 = dispatch;
// redux-promise
dispatch = (action) => {
if (action.payload instanceof Promise) {
action.payload.then((data) => {
dispatch({
...action,
payload: data,
});
});
} else {
preDispatch2(action);
}
};
// 一个 middleware 就是一个函数,用来修饰 store 的 dispatch
export const createStore = (_reducer, initialState) => {
state = initialState;
reducer = _reducer;
return store;
};
const changed = (oldState, newState) => {
let changed = false;
for (let key in oldState) {
if (oldState[key] !== newState[key]) {
changed = true;
}
}
return changed;
};
// 将组件与全局状态连接
// connect 第一个参数封装读,第二个参数封装写,两次调用可以封装读写 connectors
export const connect = (selector, dispatchSelector) => (Component) => {
return (props) => {
const [, update] = useState({});
const data = selector ? selector(state) : { state };
const dispatchers = dispatchSelector
? dispatchSelector(dispatch)
: { dispatch };
useEffect(() => {
return store.subscribe(() => {
// 检测变化,精准更新
const newData = selector ? selector(state) : { state };
if (changed(data, newData)) {
update({});
}
});
}, [selector]);
return <Component {...props} {...data} {...dispatchers} />;
};
};
export const Provider = ({ store, children }) => {
return <appContext.Provider value={store}>{children}</appContext.Provider>;
};
新的 redux-toolkit
- 减少了样板代码,配置更加简单
- reducer 内置 immer.js ,降低心智
- hooks 语法,内置 selector
- 异步请求:createAsyncThunk 和 RTK Query
- 规范化数据 createEntityAdapter
redux 复杂情况下的异步任务管理
在面对多个异步过程之间的并行、串行等复杂情况时,前文提到的 redux-thunk、redux-promise 等中间件不容易处理。而 redux-saga 提供了 all、race、takeEvery、takeLatest 等 effect 来指定多个异步过程的关系,是一个比较优秀的异步过程管理工具。个人认为相对 rxjs 这种比较成熟的异步过程管理库还是有一定差距,不过可测试性不错。复杂异步过程管理的中间件还有基于 rxjs 的 redux-observable。在 redux-saga 之上,还有 dva,集成了 redux、redux-saga、react-router-redux、react-router,建议只在复杂场景下考虑使用。
Mobx
Mobx 也是一种在 React 社区非常流行的状态管理库。它通过透明的函数响应式编程(transparently applying functional reactive programming - TFRP)使得状态管理变得简单和可扩展。
核心概念
MobX 区分了应用程序中的以下三个概念:
- State(状态)
- Actions(动作)
- Derivations(派生)
- 定义 State 并使其可观察。确保所有你想随时间改变的属性都被标记为 observable:
import { makeObservable, observable, action } from 'mobx';
class Todo {
id = Math.random();
title = '';
finished = false;
constructor(title) {
makeObservable(this, {
title: observable,
finished: observable,
toggle: action,
});
this.title = title;
}
toggle() {
this.finished = !this.finished;
}
}
- 使用 Action 更新 State
Action(动作) 是任意可以改变 State(状态) 的代码,比如用户事件处理、后端推送数据处理、调度器事件处理等等。
- 创建建 Derivations 以便自动对 State 变化进行响应
任何 来源是 State(状态) 并且不需要进一步交互的东西都是 Derivation(派生)。
Derivations 包括许多方式:
- 用户界面
- 派生数据 , 比如剩余未完成 todos 的数量
- 后端集成 , 比如发送改变到服务器端
Mobx 区分了两种 Derivation :
- Computed values,总是可以通过纯函数从当前的可观测 State 中派生。
- Reactions, 当 State 改变时需要自动运行的副作用 (命令式编程和响应式编程之间的桥梁)
Computed values 类似 Vue 中的 computed,而 Reactions 类似 watch。
autorun 和 reaction 很相似。但是 autorun 里面的函数在第一次会立即执行一次,而 reaction 不会。
实现原理
同是响应式,与 Vue 类似,包括让数据变成响应式(可观察对象)、收集依赖、派发更新这几步。
- 用 Object.defineProperty 或者 Proxy 来拦截 observable 包装的对象属性的 get/set 。
- 在 autorun 或者 reaction 执行的时候,会触发依赖状态的 get,此时将 autorun 里面的函数和依赖的状态关联起来。也就是我们常说的依赖收集。
- 当修改状态的时候会触发 set,此时会通知前面关联的函数,重新执行他们。
新的状态管理库
在 react 生态圈,状态管理库不停地涌现,个人认为比较有代表性有以下几个。
Recoil
Meta 公司内部开发的状态管理库,目前还在实验阶段。
出于兼容性和简便性的考虑,相比使用外部的全局状态,使用 React 内置的状态管理能力是个最佳的选择。但是 React 有这样一些局限性:
- 组件间的状态共享只能通过将 state 提升至它们的公共祖先来实现,但这样做可能导致重新渲染一颗巨大的组件树。
- Context 只能存储单一值,无法存储多个各自拥有消费者的值的集合。
- 以上两种方式都很难将组件树的顶层(state 必须存在的地方)与叶子组件 (使用 state 的地方) 进行代码分割。
Recoil 定义了一个有向图 (directed graph),正交同时又天然连结于你的 React 树上。状态的变化从该图的顶点(我们称之为 atom)开始,流经纯函数 (我们称之为 selector) 再传入组件。因此 Recoil 的核心概念就两个:atom 和 selector。
atom 是 Recoil 的最小粒度,一个 atom 代表一个状态,可以在任意组件中进行读写。
selector 是一个纯函数,类似于 Vue 和 Mobx 中的 computed 计算属性。它可以从 atom 或者其他 selector 里面来获取,selector 也可以被组件订阅,在变化的时候通知它们重新渲染。
如需在组件中使用 Recoil,则可以将 RecoilRoot 放置在父组件的某个位置。将他设为根组件为最佳:
import React from 'react';
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
} from 'recoil';
function App() {
return (
<RecoilRoot>
<CharacterCounter />
</RecoilRoot>
);
}
生成一个 atom:
const textState = atom({
key: 'textState', // unique ID (with respect to other atoms/selectors)
default: '', // default value (aka initial value)
});
使用,类似 React Hooks:
function CharacterCounter() {
return (
<div>
<TextInput />
<CharacterCount />
</div>
);
}
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>
);
}
Jotai
与 Recoil 类似,都是采用分散管理原子状态的设计模式。与 Recoil 相比,有以下几个优点:
- API 少,简洁易用
- 定义 Atom 时不用提供 key
- 对 TypeScript 的支持更好
使用 atom 生成一个 atom:
import { atom } from 'jotai';
const countAtom = atom(0);
然后使用 useAtom 即可像 useState 一样读和写
import { useAtom } from 'jotai';
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<h1>
{count}
<button onClick={() => setCount((c) => c + 1)}>one up</button>
</h1>
);
}
Jotai 最大的特点就是轻量、易用,很多项目其实并没有很复杂的状态需要管理,未必要使用 redux,可以考虑使用这个。
zustand
zustand 和 Jotai 一样也是 pmndrs 出品的,但是与 Jotai 分散式管理不同,zustand 还是集中统一管理,这点与 redux 类似。zustand 的优点在于不需要使用 context providers 来包裹组件。zustand 可以理解为是一个外部的世界,通过它的工厂函数创建的 hooks 与 React 世界连接。
工厂函数创建 store:
import create from 'zustand';
const useStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}));
连接到组件:
function BearCounter() {
const bears = useStore((state) => state.bears);
return <h1>{bears} around here ...</h1>;
}
function Controls() {
const increasePopulation = useStore((state) => state.increasePopulation);
return <button onClick={increasePopulation}>one up</button>;
}
与 redux 相比,样板代码非常少。
官方参考
react-reduxjs-toolkit
redux-toolkit
redux-saga
dva
zustand
jotai
其他参考
Redux 异步流最佳实践
深入理解 redux 数据流和异步过程管理
zustand 状态管理器与观察者模式
还在学 Redux?不妨提前学下它以后的替代品!——Zustand 源码解析