背景
为什么前端需要状态管理?现代主流的前端框架,不管是 react 还是 vue,都是组件化开发。组件化虽然可以提高代码复用率,但是同时也带来一个问题,那就是组件之间的通信。
父子组件之间的通信我们可以从容应对,但是当组件之间的关系变得复杂,甚至组件之间没有关系时,要在他们之间通信就变得不太容易。
这个时候我们就需要把这些在多个组件中都用到的数据提取出来,独立于组件树,放在一个新的地方。这些数据往往会因为组件上事件的触发而发生变更,同时这些数据的变更又会作用到组件上,引起视图的变更。
因此,针对这些数据的管理就变更尤为重要,这里的数据也被称为状态(state),因此也叫状态管理。
Redux
说到状态管理,那就不得不说 Redux。Redux是将整个应用的状态存储到到一个地方,称为 store。所有的数据都存在 state 中。各个组件可以派发 dispatch 行为 action 给 store,而不是直接通知其它组件。其它组件可以通过订阅 store 中的状态(state)来刷新自己的视图。没错,这就是典型的发布订阅模式。
Redux有以下几个特征:
- 整个应用的 state 被储存在一个 object 中
- state 是只读的,惟一改变 state 的方法就是触发 action
- action 是一个用于描述已发生事件的普通对象,使用纯函数来执行修改
- 为了描述 action 如何改变state ,需要编写 reducers
- 单一数据源的设计让 React 的组件之间的通信更加方便,同时也便于状态的统一管理
主要有以下几个方法:
- createStore /* 创建store*/
- combineReducers /* 组合多个reducers*/
- bindActionCreators /* 绑定action和store.dispatch*/
- applyMiddleware /* 中间件机制*/
- compose
Redux实现原理
// 创建state仓库
function createStore(reducer,preloadedState){
// reducer :action 改变 state 的动作
let currentReducer = reducer;
// 初始化状态
let currentState = preloadedState;
// 订阅的事件,当 state 变更时触发
let currentListeners = [];
// 获取当前的 state
function getState() {
return currentState;
}
// 事件订阅
function subscribe(listener) {
currentListeners.push(listener)
// 返回取消事件订阅
return function unsubscribe() {
const index = currentListeners.indexOf(listener)
currentListeners.splice(index, 1)
}
}
// 派发 action
function dispatch(action) {
currentState = currentReducer(currentState, action)
for (let i = 0; i < currentListeners.length; i++) {
const listener = currentListeners[i];
listener();
}
return action;
}
// 初始化状态
dispatch({ type: ActionTypes.INIT });
const store = ({
dispatch,
subscribe,
getState
})
// 返回store
return store
}
Redux使用案例
import { createStore} from 'redux';
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
let initState = { number: 0 };
const reducer = (state = initState, action) => {
switch (action.type) {
case INCREMENT:
return { number: state.number + 1 };
case DECREMENT:
return { number: state.number - 1 };
default:
return state;
}
}
let store = createStore(reducer);
function render() {
console.log(store.getState().number);
}
store.subscribe(render);
render();
store.dispatch({ type: INCREMENT });
store.dispatch({ type: DECREMENT });
总结一下 Redux 的用法:
- 1.定义 reducer,建立 action 改变 state 的规则
- 2.createStore 创建 store
- 3.subscribe 订阅函数,当 state 变化时触发
- 4.dispatch 方法派发 action
React-Redux
Redux 是一个状态管理的库,但是它不针对于任何一个框架。如果我们要在 react 中使用 store,就需要在每一个用到的组件中引入,这样用起来比较麻烦。因此针对 React 在 Redux 的基础上诞生了 React-Redux。React-Redux 主要由以下几个部分组成:
- ReactReduxContext 函数
创建全局上下文
import React from 'react';
export const ReactReduxContext = React.createContext(null)
export default ReactReduxContext;
- Provider 组件
把 store 挂载到全局上下文
import React from 'react'
import ReactReduxContext from './ReactReduxContext';
export default function(props){
return (
<ReactReduxContext.Provider value={{ store: props.store }}>
{props.children}
</ReactReduxContext.Provider>
)
}
- connect 函数
connect 函数接受两个参数:mapStateToProps 和 mapDispatchToProps。mapStateToProps 是从state 对象中筛选出当前需要的属性;mapDispatchToProps 当前需要用到的 actions。返回的是一个高阶函数,它接受一个组件为参数,返回一个函数组件。相当于把原来的组件经过包装之后,变成了拥有 store 中特定 state 和 action 的组件。
import React, { useContext, useMemo, useLayoutEffect, useReducer } from "react";
import { bindActionCreators } from "../redux";
import ReactReduxContext from "./ReactReduxContext";
export default function (mapStateToProps, mapDispatchToProps) {
return function (WrappedComponent) {
return function (props) {
const { store } = useContext(ReactReduxContext);
const { getState, dispatch, subscribe } = store;
const prevState = getState();
const stateProps = useMemo(() => mapStateToProps(prevState), [prevState]);
const dispatchProps = useMemo(() => {
let dispatchProps;
if (typeof mapDispatchToProps === "object") {
dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
} else if (typeof mapDispatchToProps === "function") {
dispatchProps = mapDispatchToProps(dispatch, props);
} else {
dispatchProps = { dispatch };
}
return dispatchProps;
}, [dispatch,props]);
const [, forceUpdate] = useReducer(x => x + 1, 0)
useLayoutEffect(() => subscribe(forceUpdate), [subscribe]);
return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />;
}
};
}
总结一下 React-Redux:
- 1.借助 Provider 组件,使得子组件可以获得 store 实例
- 2.通过 connect 函数,以高阶组件的方式传递特定的 state 和 actions。
截止目前,Redux 和 React-Redux 中所接触的概念还是比较容易理解的,并不算太难。可是 Redux 中还有一个不成文的规定,那就是 reducers 函数必须是纯函数。
纯函数是函数式编程的概念,只要是同样的输入,必定得到同样的输出。同时函数执行过程中不能产生副作用,比如修改参数,或者向后端发起请求等。Reducer 是纯函数,就可以保证同样的 state,必定得到同样的 view。但也正因为这一点,reducer 函数里面不能改变 State,必须返回一个全新的对象。
那该如何解决副作用的问题呢?Redux 中间件就此诞生。
Redux 中间件
如果没有中间件的运用,redux 的工作流程是这样 action -> reducer,这是相当于同步操作,由dispatch 触发action后,直接去reducer执行相应的动作。
但是在某些比较复杂的业务逻辑中,这种同步的实现方式并不能很好的解决我们的问题。比如我们有一个这样的需求,点击按钮 -> 获取服务器数据 -> 渲染视图,因为获取服务器数据是需要异步实现,所以这时候我就需要引入中间件,改变 redux 同步执行的流程,形成异步流程来实现我们所要的逻辑。
有了中间件,redux 的工作流程就变成这样 action -> middlewares -> reducer,点击按钮就相当于dispatch 触发action,接下去获取服务器数据 middlewares 的执行,当 middlewares 成功获取到服务器就去触发reducer对应的动作,更新需要渲染视图的数据。
中间件的机制可以让我们改变数据流,实现如异步 action ,action 过滤,日志输出,异常报告等功能。
function applyMiddleware(...middlewares){
return function(createStore){
return function(reducer){
let store = createStore(reducer);
let dispatch;
let middlewareAPI= {
getState:store.getState,
dispatch:(action)=>dispatch(action);
let chain = middlewares.map(middleware=>middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch
};
}
}
}
export default applyMiddleware;
中间件书写样例 redux-thunk
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
- 使用:接受参数用户名,并返回一个函数(参数为dispatch)
const login = (userName) => (dispatch) => {
dispatch({ type: 'loginStart' })
request.post('/api/login', { data: userName }, () => {
dispatch({ type: 'loginSuccess', payload: userName })
})
}
store.dispatch(login('ant'))
大家是否好奇为什么会出现一个 redux-thunk ?
(3 Jun 2015)
其实,最开始的时候,redux-thunk 还没有独立,而是写在 redux 的 action 分发函数中的一个代码分支而已,和现在的逻辑一样。现在将redux-thunk独立出去,用 middleware 的方式实现,会让 redux 更纯。
Redux-saga
It is a Redux middleware for handling side effects. —— Yassine Elouafi Redux-saga 是 redux 的中间件,主要就是用来处理副作用(异步任务)。
特点:
- sages 采用 Generator 函数来 yield Effects(包含指令的文本对象)
- Generator 函数的作用是可以暂停执行,再次执行的时候从上次暂停的地方继续执行
- Effect 是一个简单的对象,该对象包含了一些给 middleware 解释执行的信息。
- 可以通过使用 effects API 如 fork,call,take,put,cancel 等来创建 Effect。
与 redux-thunk 不同之处:
- 将所有的异步流程控制都移入到了 sagas,UI 组件不用执行业务逻辑,只需 dispatch action 就行,增强组件复用性。
- UI 组件不用执行业务逻辑,只需 dispatch 一个 plain Object 的 action。
- 借助 Generator 函数和自身的 effect 实现复杂控制流
缺点:
- 概念太多,不容易理解,上手成本高
- 在业务中需要写很多 saga
Ps: saga 本意是表示使用分布式事务管理长期运行的业务流程。
The term saga, in relation to distributed systems, was originally defined in the paper "Sagas" by Hector Garcia-Molina and Kenneth Salem. This paper proposes a mechanism that it calls a saga as an alternative to using a distributed transaction for managing a long-running business process.The paper recognizes that business processes are often comprised of multiple steps, each of which involves a transaction, and that overall consistency can be achieved by grouping these individual transactions into a distributed transaction.
MobX
核心思想
MobX背后的哲学很简单: 任何源自应用状态的东西都应该自动地获得。
MobX 支持单向数据流,也就是动作改变状态,而状态的改变会更新所有受影响的视图。
特点: 不同于其他的框架,MobX 对于如何处理用户事件是完全自由的。可以以最直观、最简单的方式来处理事件,直接修改 state。最后全部归纳为: 状态应该以某种方式来更新。
当状态更新后,MobX 会以一种高效且无障碍的方式处理好剩下的事情。像下面如此简单的语句,已经足够用来自动更新用户界面了。
从技术上层面来讲,并不需要触发事件、调用分派程序或者类似的工作。归根究底 React 组件只是状态的华丽展示,而状态的衍生由 MobX 来管理。
Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
const store = createStore({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
Ps: Vuex 中为什么把异步操作封装在 action,把同步操作放在 mutations?
dva
数据流向
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State。
app.model({
namespace: 'count',
state: {
record: 0,
current: 0,
},
reducers: {
add(state) {
const newCurrent = state.current + 1;
return { ...state,
record: newCurrent > state.record ? newCurrent : state.record,
current: newCurrent,
};
},
minus(state) {
return { ...state, current: state.current - 1};
},
},
effects: {
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},
subscriptions: {
keyboardWatcher({ dispatch }) {
key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
},
},
});
对比 vuex 和 dva ,我们可以发现,这两者的书写样式还是比较相似的。也是相对来说比较友好的一种方式。
Hooks 时代的 React 状态管理方案
随着 Hooks 的发布,React 社区又掀起了一波基于 Hooks 的造轮子大潮。
在 React Europe 2020 Conference上,Facebook 软件工程师 Dave McCabe 介绍了一个新的状态管理库Recoil。Recoil 现在还处于实验阶段,当前最新版本为 0.2.0,现在已经在 Facebook 一些内部产品中用于生产环境。
像 Redux、Mobx 本身虽然提供了强大的状态管理能力,但是使用的成本非常高,需要编写大量冗长的代码,另外像异步处理或缓存计算也不是这些库本身的能力,甚至需要借助其他的外部库。并且,它们并不能访问 React内部的调度程序,而 Recoil 在后台使用 React 本身的状态,在未来还能提供并发模式这样的能力。
毕竟是 Facebook 官方推出的状态管理框架,其主打的是高性能以及可以利用 React 内部的调度机制,包括其承诺即将会支持的并发模式,所以还是值得期待的。 设计理念
场景:有 List 和 Canvas 两个组件,List 中一个节点更新后,Canvas 中的节点也对应更新。
常规则做法是将一个state通过父组件分发给List和Canvas两个组件,显然这样的话每次state改变后 所有节点都会全量更新。
Recoil 本身就是为了解决 React 全局数据流管理的问题,采用分散管理原子状态的设计模式。改变一个原子状态只会渲染特定的子组件,并不会让整个父组件重新渲染。
核心概念
-
Atoms Atom 是最小状态单元。它们可以被订阅和更新:当它更新时,所有订阅它的组件都会使用新数据重绘;它可以在运行时创建;它也可以在局部状态使用;同一个 Atom 可以被多个组件使用与共享。
-
Selectors Selector 是一个入参为 Atom 或者其他 Selector 的纯函数。当它的上游 Atom 或者 Selector 更新时,它会进行重新计算。Selector 可以像 Atom 一样被组件订阅,当它更新时,订阅它的组件将会重新渲染。 Selector 通常用于计算一些基于原始状态的派生数据。因为不需要使用 reducer 来保证数据的一致性和有效性,所以可以避免冗余数据。我们使用 Atom 保存一点原始状态,其他数据都是在其基础上计算得来的。因为 Selector 会追踪使用它们的组件以及它们依赖的数据状态,所以函数式编程会比较高效。
-
RecoilRoot 组件使用 Recoil 状态之前需要在它的外面包裹一层RecoilRoot组件,可以直接短平快地放在根组件外面。
-
useRecoilState 基于 Atom 和 Selector 的 Hooks,返回对应的 state 和修改 state 的方法。
import {
atom,
selector,
RecoilRoot,
useRecoilState,
useRecoilValue
} from "recoil";
import React from "react";
// atom
const fontSizeState = atom({
key: "fontSizeState",
default: 14
});
// selector
const fontSizeLabelState = selector({
key: "fontSizeLabelState",
get: ({ get }) => {
const fontSize = get(fontSizeState);
const unit = "px";
return `${fontSize}${unit}`;
}
});
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
const fontSizeLabel = useRecoilValue(fontSizeLabelState);
return (
<>
<div>当前字号(atom): {fontSize}</div>
<div>当前字号(selector): {fontSizeLabel}</div>
<button onClick={() => setFontSize(fontSize + 1)} style={{ fontSize }}>
增大字号
</button>
</>
);
}
function Text() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
return <p style={{ fontSize }}>这里的字号会同步增大:{fontSize}</p>;
}
// RecoilRoot
export default function App() {
return (
<RecoilRoot>
<FontButton />
<Text />
</RecoilRoot>
);
}