主要介绍两个状态管理方案:
- Redux
- Mobx6
Redux是JavaScript的状态容器,提供可预测化的状态管理
核心状态和工作流程: Store:存储状态的容器,JavaScript对象 View: 视图,HTML⻚⾯ Actions: 对象,描述对状态进⾏怎样的操作 Reducers:函数,操作状态并返回新的状态
在React中组件通信的数据流是单向的, 顶层组件可以通过props属性向下层组件传递数据, ⽽下层组件不能向上层组件传递数据, 要实现下层组件修改数据, 需要上层组件传递修改数据的⽅法到下层组件. 当项⽬越来越⼤的时候, 组件之间传递数据变得越来越困难.
使⽤Redux管理数据,由于Store独⽴于组件,使得数据管理独⽴于组件,解决了组件与组件之间传递数据困难的问题。
- 组件通过 dispatch ⽅法触发 Action
- Store 接收 Action 并将 Action 分发给 Reducer
- Reducer 根据 Action 类型对状态进⾏更改并将更改后的状态返回给 Store
- 组件订阅了Store中的状态,Store中的状态更新会同步到组件
const reducer = (state=initState, action) => {
switch(action.type) {
// ...
}
};
const store = Redux.createStore(reducer);
store.getState();
store.subscribe(() => {});
store.dispatch({ type: 'xxx', payload: {}});
创建context
import { Provider } from 'react-redux';
ReactDOM.render(<Provider store={ store }><App /></Provider>);
通过connect高阶组件传递store的state给组件
import { connect } from 'react-redux';
const mapStateToProps = state => {};
const mapDispatchToProps = dispatch => {};
const connectedX = connect(mapStateToProps, mapDispatchToProps)(X);
中间件允许我们扩展redux应⽤程序。
加入了中间件的工作流程:
中间件模板代码:
export default store => next => action => {}
注册并且使用中间件
import { createStore, applymiddleware } from 'redux'
import logger from 'middlewares/logger';
const enhancer = applyMiddleware(logger)
createStore(reducer, 'initState', enhancer);
例子:thunk,允许我们在redux的工作流程中加入异步代码,如果action是是function就执行action(),否则就当做正常同步的action来执行。
// 模拟thunk的实现
const thunk = ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
next(action);
}
使用thunk
// 创建一个async action,如果dispatch一个function,就会执行它并且传入dispatch和getState当做实参。
const loadPost = id => async (dispatch, getState) => {
const posts = await axios.get(`/api/post/${id}`).then(res => res.data);
dispatch({ type: LOAD_SUCCESS, payload: posts });
}
// 调用
dispatch(loadPosts(3008))
常用的中间件还有redux saga,主要是用generator来替代async function。 常用的工具:redux-actions,可以简化Action和Reducer的处理,有点类似之前meraki自己写的那些模板,可以一键创建data的模板和各种reducers。
createStore
基本上就是存了state和listeners,更新状态的时候调用reducer(state, action)
拿到新的state,然后通知调用listeners的callbacks。
applyMiddleware applyMiddleware主要是返回一个新的dispatcher,store的其它的api都不改变。这个新的dispatcher就是chain,调用的时候会一个一个调用middleware,然后最后调用最原始的dispatcher。
bindActionCreators 这个有个小技巧,具体看代码注释
function createStore (reducer, preloadedState, enhancer) {
if (typeof reducer !== 'function') throw new Error('redcuer必须是函数');
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('enhancer必须是函数')
}
return enhancer(createStore)(reducer, preloadedState);
}
let state = preloadedState; // 状态
let listeners = []; // 订阅者
const getState = () => state;
const subscribe = listener => {
listeners.push(listener);
let isSubscribed = true;
return function unsubscribe() {
if (!isSubscribed) return;
isSubscribed = false;
const idx = listeners.indexOf(listener);
listeners.spilce(idx, 1);
}
}
const dispatch = (action) => {
if (!isPlainObject(action)) throw new Error('action必须是一个对象');
if (typeof action.type === 'undefined') throw new Error('action对象中必须有type属性');
state = reducer(state, action);
listeners.forEach(cb => cb());
}
// 默认调用一次dispatch方法 存储初始状态(通过reducer函数传递的默认状态)
dispatch({ type: 'initAction' })
return {
getState,
dispatch,
subscribe,
}
};
// 判断参数是否是对象类型
// 判断对象的当前原型对象是否和顶层原型对象相同
// redux源码里面就这样写的
function isPlainObject (obj) {
if (typeof obj !== 'object' || obj === null) return false;
// 区分数组和对象
var proto = obj;
while (Object.getPrototypeOf(proto) != null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto;
}
function applyMiddleware (...middlewares) {
return function (createStore) {
return function (reducer, preloadedState) {
// 创建 store
const store = createStore(reducer, preloadedState);
// 阉割版的 store
const middlewareAPI = {
getState: store.getState,
dispatch: store.dispatch
}
// 调用中间件的第一层函数 传递阉割版的store对象
// chian里面现在存了middleware的下面两层函数
const chain = middlewares.map(middleware => middleware(middlewareAPI));
const dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch
}
}
}
}
function compose(...chain) {
// var chain = [...arguments]; // 伪数组转换成真数组
/* ----------
假设调用的时候是applyMiddleware(x, y, z):
这里反着循环,最后一个middleware(z)的next参数就是reducerDispatch,真正的对于reducer的dispatch
倒数第二个middleware(y)的next参数就是z
......
最后返回第一个middleware(x)的dispatch,实际上是x最里层的函数action => { doSth(); next(action)) }
调用的时候就会一直next(action)下去,middleware执行完以后,最后一个next(action)实际上就是reducerDispatch(action)
---------- */
return reducerDispatch => {
let dispatch = reducerDispatch;
for (let i = chain.length - 1; i >= 0; i--) {
dispatch = chain[i](dispatch);
}
return dispatch;
}
}
function bindActionCreators (actionCreators, dispatch) {
var boundActionCreators = {};
for (var key in actionCreators) {
// 这里如果不用IIFE的话,actionCreators[key]的key永远会是指向循环最后的key
// 用IIFE可以让每次的key不要释放,每个actionCreators[key]就会指向正确的key
// 其实就是闭包,原理就是没有IIFE的时候,() => dispatch(actionCreators[key]()) 这个函数在创建的时候是没有调用key的,在函数执行的时候才调用key,这个时候key就会指向最后一个key。但是IIFE的话就每次都调用了key,等于缓存的当时那个snapshot的key值
(function (key) {
boundActionCreators[key] = function () {
dispatch(actionCreators[key]())
}
})(key)
}
return boundActionCreators;
}
function combineReducers (reducers) {
// 1. 检查reducer类型 它必须是函数
const reducerKeys = Object.keys(reducers);
reducerKeys.forEach(key => {
if (typeof reducers[key] !== 'function') throw new Error('reducer必须是函数');
})
// 2. 调用每一个小的reducer 将每一个小的reducer中返回的状态存储在一个新的大的对象中
// redux 源码里面就这么做的……
return function (state, action) {
const newState = {};
reducerKeys.forEach(key => {
const reducer = reducers[key];
const prevPartialState = state[key];
const newPartialState = reducer(prevPartialState, action);
newState[key] = newPartialState
});
return newState;
}
}
- 单一事实来源:整个应用的状态存储在单个 store 中的对象/状态树里。单一状态树可以更容易地跟踪随时间的变化,并调试或检查应用程序。不像组件传来穿去的话就会有多个事实来源,很难debug而且数据流混乱。
- 状态是只读的:改变状态的唯一方法是去触发一个动作。动作是描述变化的普通 JS 对象。就像 state 是数据的最小表示一样,该操作是对数据更改的最小表示。
- 使用纯函数进行更改:为了指定状态树如何通过操作进行转换,你需要纯函数。纯函数是那些返回值仅取决于其参数值的函数。好处是可以track每个state的改变,容易debug。也就是可预测的状态变化。
拓展:redux的优势:
- 结果的可预测性:加上reducer的pure属性,让state变化本身可预测。只存在一个真实来源,即 store ,让component拿到的状态也可预测,不存在如何将当前状态与动作和应用的其他部分同步的问题。
- **可维护性:**因为可预测,所以更可未婚。
- **易于测试:**Redux 的代码主要是小巧、纯粹和独立的功能。这使代码可测试且独立。
- 服务器端渲染:你只需将服务器上创建的 store 传到客户端即可。这对初始渲染非常有用,并且可以优化应用性能,从而提供更好的用户体验。
- 开发人员工具:从操作到状态更改,开发人员可以实时跟踪应用中发生的所有事情。
- 社区和生态系统:Redux 背后有一个巨大的社区,这使得它更加迷人。一个由才华横溢的人组成的大型社区为库的改进做出了贡献,并开发了各种应用。
- 组织:Redux 准确地说明了代码的组织方式,这使得代码在团队使用时更加一致和简单。## Redux 有哪些优点?
共同点
- 为了解决状态管理混乱,无法有效同步的问题统一维护管理应用状态
- 某一状态只有一个可信数据来源(通常命名为store,指状态容器
- 操作更新状态方式统一,并且可控(通常以action方式提供更新状态的途径)
- 支持将store与React组件连接,如react-redux,mobx-react
区别
- Redux更多的是遵循Flux模式的一种实现,是一个 JavaScript库。Mobx是一个响应式编程的状态管理库,它使得状态管理简单可伸缩。
- redux将数据保存在单一的store中,mobx将数据保存在分散的多个store中
- redux使用plain object保存数据,需要手动处理变化后的操作。mobx适用observable保存数据,数据变化后自动处理响应的操作
- redux使用不可变状态,这意味着状态是只读的,不能直接去修改它,而是应该返回一个新的状态,同时使用纯函数。mobx中的状态是可变的,可以直接对其进行修改
- mobx相对来说比较简单,在其中有很多的抽象,mobx更多的使用面向对象的编程思维。redux会比较复杂,因为其中的函数式编程思想掌握起来不是那么容易,同时需要借助一系列的中间件来处理异步和副作用
- mobx中有更多的抽象和封装,调试会比较困难,同时结果也难以预测。而redux提供能够进行时间回溯的开发工具,同时其纯函数以及更少的抽象,让调试变得更加的容易
本质上,redux与vuex都是对mvvm思想的服务,将数据从视图中抽离的一种方案
共同思想
- 单一的数据源
- 变化可以预测
区别
- Vuex改进了Redux中的Action和Reducer函数,以mutations变化函数取代Reducer,无需switch,只需在对应的mutation函数里改变state值即可
- Vuex由于Vue自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的State即可
- Vuex数据流的顺序是∶View调用store.commit提交对应的请求到Store中对应的mutation函数->store改变(vue检测到数据变化自动渲染)
通俗点理解就是,vuex 弱化 dispatch,通过commit进行 store状态的一次更变;取消了action概念,不必传入特定的 action形式进行指定变更;弱化reducer,基于commit参数直接对数据进行转变,使得框架更加简易;
redux中间件本质就是一个函数柯里化。redux applyMiddleware Api 源码中每个middleware 接受2个参数, Store 的getState 函数和dispatch 函数,分别获得state和action,最终返回一个函数。该函数会被传入 next (下一个 middleware 的 dispatch 方法),并返回一个接收 action 的新函数,这个函数可以直接调用 next(action),或者在其他需要的时刻调用,甚至根本不去调用它。调用链中最后一个 middleware 会接受真实的 store的 dispatch 方法作为 next 参数,并借此结束调用链。所以,middleware 的形式是
({ getState,dispatch }) => next => action
connect负责连接React和Redux,具体做了三件事:
- 获取state。connect 通过 context获取 Provider 中的 store,通过 store.getState() 获取整个store tree 上所有state。
- 包装原组件。将state和action通过props的方式传入到原组件内部 wrapWithConnect 返回—个 ReactComponent 对 象 Connect,Connect 重 新 render 外部传入的原组件 WrappedComponent ,并把 connect 中传入的 mapStateToProps,mapDispatchToProps与组件上原有的 props合并后,通过属性的方式传给WrappedComponent
- 监听store tree变化connect缓存了store tree中state的状态,通过当前state状态 和变更前 state 状态进行比较,从而确定是否调用 this.setState()方法触发Connect及其子组件的重新渲染
const connect = (mapStateToProps, mapDispatchToProps) => (BaseComponent) => {
return class Connect extends React.Component {
// 通过对context调用获取store
static contextTypes = {
store: PropTypes.object
}
constructor() {
super()
this.state = {
allProps: {}
}
}
// 第一遍需初始化所有组件初始状态
componentWillMount() {
const store = this.context.store;
store.subscribe(() => this.updateProps());
this.updateProps();
}
// 执行action后更新props,使组件可以更新至最新状态(类似于setState)
updateProps() {
const store = this.context.store;
const stateProps = mapStateToProps
? mapStateToProps(store.getState(), this.props)
: {}
const defaultDispatchProps = { dispatch: store.dispatch };
const dispatchProps = mapDispatchToProps
? {
...defaultDispatchProps,
...mapDispatchToProps(store.dispatch, this.props)
}
: defaultDispatchProps
this.setState({
allProps: {
...stateProps,
...dispatchProps,
...this.props
}
})
}
render() {
return <BaseComponent { ...this.state.allProps } />
}
}
}
MobX 是一个简单的可扩展的状态管理库,无样板代码风格简约。
目前最新版本为 6,版本 4 和版本 5 已不再支持。
在 MobX 6 中不推荐使用装饰器语法,因为它不是 ES 标准,并且标准化过程要花费很长时间,但是通过配置仍然可以启用装饰器语法。
MobX 可以运行在任何支持 ES5 的环境中,包含浏览器和 Node。
MobX 通常和 React 配合使用,但是在 Angular 和 Vue 中也可以使用 MobX。
- observable:被 MobX 跟踪的状态。
- action:允许修改状态的方法,在严格模式下只有 action 方法被允许修改状态。
- computed:根据现有状态衍生出来的状态。
- flow:执行副作用,它是 generator 函数。可以更改状态值。
在组件中显示数值状态,单击[+1]按钮使数值加一,单击[-1]按钮使数值减一。
- 通过 observable 标识状态,使状态可观察
- 通过 action 标识修改状态的方法,状态只有通过 action 方法修改后才会通知视图更新
import { action, makeObservable, observable } from "mobx"
export default class CounterStore {
constructor() {
this.count = 0
makeObservable(this, {
count: observable,
increment: action,
decrement: action
})
}
increment() {
this.count += 1
}
decrement() {
this.count -= 1
}
}
创建 Store 类的实例对象并将实例对象传递给组件
// App.js
import Counter from "./Counter"
import CounterStore from "../store/Counter"
const counterStore = new CounterStore()
function App() {
return <Counter counterStore={counterStore} />
}
export default App
在组件中通过 Store 实例对象获取状态以及操作状态的方法
function Counter({ counterStore }) {
return (
<Container>
<Button onClick={() => counterStore.increment()}>
INCREMENT
</Button>
<Button>{counterStore.count}</Button>
<Button onClick={() => counterStore.decrement()}>
DECREMENT
</Button>
</Container>
)
}
export default Counter
当组件中使用到的 MobX 管理的状态发生变化后,使视图更新。通过 observer 方法包裹组件实现目的
import { observer } from "mobx-react-lite"
function Counter() { }
export default observer(Counter)
function Counter({ counterStore }) {
const { count, increment, decrement } = counterStore
return (
<Container>
<Button border="left" onClick={increment}>
INCREMENT
</Button>
<Button>{count}</Button>
<Button border="right" onClick={decrement}>
DECREMENT
</Button>
</Container>
)
}
当代码如上简化(解构 store)后,修改状态的方法中的 this 指向出现了问题,通过 action.bound 强制绑定 this,使 this 指向 Store 实例对象
import { action, makeObservable, observable } from "mobx"
export default class CounterStore {
constructor() {
this.count = 0
makeObservable(this, {
count: observable,
increment: action.bound,
decrement: action.bound
})
}
increment() {
this.count += 1
}
decrement() {
this.count -= 1
}
}
总结:状态变化更新视图的必要条件
- 状态必须被标记为
observable
- 更改状态的方法必须被标记为
action
- 组件必须通过
observer
方法包裹
在应用中可存在多个 Store,多个 Store 最终要通过 RootStore 管理,在每个组件都需要获取到 RootStore。
// store/index.js
import { createContext, useContext } from "react"
import CounterStore from "./Counter"
class RootStore {
constructor() {
this.counterStore = new CounterStore()
}
}
const rootStore = new RootStore()
const RootStoreContext = createContext()
export const RootStoreProvider = ({ children }) => {
return (
<RootStoreContext.Provider value={rootStore}>
{children}
</RootStoreContext.Provider>
)
}
export const useRootStore = () => {
return useContext(RootStoreContext)
}
// App.js
import { RootStoreProvider } from "../store"
import Counter from "./Counter"
function App() {
return (
<RootStoreProvider>
<Counter />
</RootStoreProvider>
)
}
export default App
import { observer } from "mobx-react-lite"
import { useRootStore } from "../store"
function Counter() {
const { counterStore } = useRootStore()
const { count, increment, decrement } = counterStore
return (
<Container>
<Button onClick={increment}>
INCREMENT
</Button>
<Button>{count}</Button>
<Button onClick={decrement}>
DECREMENT
</Button>
</Container>
)
}
export default observer(Counter)