在⼀切开始之前,我们⾸先要回答⼀个问题:为什么我们需要redux,redux为我们解决了什么问题?只有回答了这个 问题,我们才能把握redux的设计思路。
React作为⼀个组件化开发框架,组件之间存在⼤量通信,有时这些通信跨越多个组件,或者多个组件之间共享⼀套数 据,简单的⽗⼦组件间传值不能满⾜我们的需求,⾃然⽽然地,我们需要有⼀个地⽅存取和操作这些公共状态。⽽ redux就为我们提供了⼀种管理公共状态的⽅案 ,我们后续的设计实现也将围绕这个需求来展开。
我们思考⼀下如何管理公共状态:既然是公共状态,那么就直接把公共状态提取出来好了。我们创建⼀个store.js⽂ 件,然后直接在⾥边存放公共的state,其他组件只要引⼊这个store就可以存取共⽤状态了。
const state = {
count: 0
}
我们在store⾥存放⼀个公共状态count,组件在import了store后就可以操作这个count。这是最直接的store,当然我 们的store肯定不能这么设计,原因主要是两点:
1.容易误操作
⽐如说,有⼈⼀个不⼩⼼把store赋值了{},清空了store,或者误修改了其他组件的数据,那显然不太安全,出错了也 很难排查,因此我们需要有条件地操作store,防⽌使⽤者直接修改store的数据。
state = "for the horde!!" // 哦豁
2.可读性很差
JS是⼀⻔极其依赖语义化的语⾔,试想如果在代码中不经注释直接修改了公⽤的state,以后其他⼈维护代码得多懵 逼,为了搞清楚修改state的含义还得根据上下⽂推断,所以我们最好是给每个操作起个名字。
我们重新思考⼀下如何设计这个公共状态管理器,根据我们上⾯的分析,我们希望 公共状态既能够被全局访问到,⼜是私有的不能被 直接修改 ,思考⼀下, 闭包 是不是就就正好符合这两条要求,因此我们会把公共状态设计成闭包。
既然我们要存取状态,那么肯定要有 getter 和 setter ,此外当状态发⽣改变时,我们得进⾏⼴播,通知组件状态发⽣ 了变更。这不就和redux的三个API: getState、dispatch、subscribe 对应上了吗。我们⽤⼏句代码勾勒出store的⼤致形 状:
export const createStore = () => {
let currentState = {} // 公共状态
function getState() {} // getter
function dispatch() {} // setter
function subscribe() {} // 发布订阅
return { getState, dispatch, subscribe }
}
1. getState实现
getState() 的实现⾮常简单,返回当前状态即可:
function getState() { return currentState }
2. dispatch实现
但是 dispatch() 的实现我们得思考⼀下,经过上⾯的分析,我们的⽬标是 有条件地、具名地 修改store的数据,那么我们要 如何实现这两点呢?我们已经知道,在使⽤dispatch的时候,我们会给dispatch()传⼊⼀个action对象,这个对象包括 我们要修改的state以及这个操作的名字(actionType),根据type的不同,store会修改对应的state。我们这⾥也沿⽤ 这种设计:
function dispatch(action) {
switch (action.type) {
case 'plus':
currentState = {
...state,
count: currentState.count + 1
}
}
}
我们把对actionType的判断写在了dispatch中,这样显得很臃肿,也很笨拙,于是我们想到把这部分修改state的规则 抽离出来放到外⾯,这就是我们熟悉的reducer。我们修改⼀下代码,让reducer从外部传⼊:
import { reducer } from './reducer'
export const createStore = (reducer) => {
let currentState = {}
function getState() {
return currentState
}
function dispatch(action) {
currentState = reducer(currentState, action)
}
function subscribe() {}
return { getState, dispatch, subscribe }
}
然后我们创建⼀个reducer.js⽂件,写我们的reducer
//reducer.js
const initialState = {
count: 0
}
export function reducer(state = initialState, action) {
switch(action.type) {
case 'plus':
return {
...state,
count: state.count + 1
}
case 'subtract':
return {
...state,
count: state.count - 1
}
default:
return initialState
}
}
代码写到这⾥,我们可以验证⼀下getState和dispatch:
//store.js
import { reducer } from './reducer'
export const createStore = (reducer) => {
let currentState = {}
function getState() {
return currentState
}
function dispatch(action) {
currentState = reducer(currentState, action)
}
function subscribe() {}
return { getState, subscribe, dispatch }
}
const store = createStore(reducer) //创建store
store.dispatch({ type: 'plus' }) //执⾏加法操作,给count加1
console.log(store.getState()) //获取state
运⾏代码,我们会发现,打印得到的state是:{ count: NaN },这是由于store⾥初始数据为空,state.count + 1实际 上是underfind+1,输出了NaN,所以我们得先进⾏store数据初始化,我们在执⾏dispatch({ type: 'plus' })之前先进⾏⼀次初始化的dispatch,这个dispatch的actionType可以随便填,只要不和已有的type重复, 让reducer⾥的switch能⾛ 到default去初始化store 就⾏了:
import { reducer } from './reducer'
export const createStore = (reducer) => {
let currentState = {}
function getState() {
return currentState
}
function dispatch(action) {
currentState = reducer(currentState, action)
}
function subscribe() {}
dispatch({ type: '@@REDUX_INIT' }) //初始化store数据
return { getState, subscribe, dispatch }
}
const store = createStore(reducer) //创建store
store.dispatch({ type: 'plus' }) //执⾏加法操作,给count加1
console.log(store.getState()) //获取state
运⾏代码,我们就能打印到的正确的state:{ count: 1 }
3.subscribe实现
尽管我们已经能够存取公⽤state,但store的变化并不会直接引起视图的更新,我们需要监听store的变化,这⾥我们 应⽤⼀个设计模式——观察者模式,观察者模式被⼴泛运⽤于监听事件实现。
所谓观察者模式,概念也很简单: 观察者监听被观察者的变化,被观察者发⽣改变时,通知所有的观察者。
我们每次dispatch,都进⾏⼴播,通知组件store的状态发⽣了变更。
import { reducer } from './reducer'
export const createStore = (reducer) => {
let currentState = {}
let observers = [] //观察者队列
function getState() {
return currentState
}
function dispatch(action) {
currentState = reducer(currentState, action)
observers.forEach(fn => fn())
}
function subscribe(fn) {
observers.push(fn)
}
dispatch({ type: '@@REDUX_INIT' }) //初始化store数据
return { getState, subscribe, dispatch }
}
我们来试⼀下这个subscribe(这⾥就不创建组件再引⼊store再subscribe了,直接在store.js中模拟⼀下两个组件使⽤ subscribe订阅store变化):
import { reducer } from './reducer'
export const createStore = (reducer) => {
let currentState = {}
let observers = [] //观察者队列
function getState() {
return currentState
}
function dispatch(action) {
currentState = reducer(currentState, action)
observers.forEach(fn => fn())
}
function subscribe(fn) {
observers.push(fn)
}
dispatch({ type: '@@REDUX_INIT' }) //初始化store数据
return { getState, subscribe, dispatch }
}
const store = createStore(reducer) //创建store
store.subscribe(() => { console.log('组件1收到store的通知') })
store.subscribe(() => { console.log('组件2收到store的通知') })
store.dispatch({ type: 'plus' }) //执⾏dispatch,触发store的通知
控制台成功输出store.subscribe()传⼊的回调的执⾏结果:
到这⾥,⼀个简单的redux就已经完成,在redux真正的源码中还加⼊了⼊参校验等细节,但总体思路和上⾯的基本相 同。
我们已经可以在组件⾥引⼊store进⾏状态的存取以及订阅store变化,数⼀下,⼗六⾏代码。
尽管说我们已经实现了redux,但并不满⾜于此,我们在使⽤store时,需要在每个组件中引⼊store,然后getState, 然后dispatch,还有subscribe,代码⽐较冗余,我们需要合并⼀些重复操作,⽽其中⼀种简化合并的⽅案,就是我们 熟悉的react-redux。
react-redux 的实现
react-redux提供 Provider 和 connect 两个API,
- Provider将store放进this.context⾥,省去了import这⼀步,
- connect将getState、dispatch合并进了this.props,并⾃动订阅更新,简化了另外三步。
下⾯我们来看⼀下如何实现这两个API:
1. Provider实现
我们先从⽐较简单的 Provider 开始实现,Provider是⼀个组件,接收store并放进全局的 context 对象,⾄于为什么要放 进context,后⾯我们实现connect的时候就会明⽩。
import React from 'react'
import PropTypes from 'prop-types'
export class Provider extends React.Component {
// 需要声明静态属性childContextTypes来指定context对象的属性,是context的固定写法
static childContextTypes = {
store: PropTypes.object
}
// 实现getChildContext⽅法,返回context对象,也是固定写法
getChildContext() {
return { store: this.store }
}
constructor(props) {
super(props)
this.store = props.store
}
// 渲染被Provider包裹的组件
render() {
return this.props.children
}
}
完成Provider后,我们就能在组件中通过this.context.store这样的形式取到store,不需要再单独import store。
2. connect实现
下⾯我们来思考⼀下如何实现 connect ,我们先回顾⼀下connect的使⽤⽅法:
connect(mapStateToProps, mapDispatchToProps)(App)
我们已经知道,connect接收mapStateToProps、mapDispatchToProps两个⽅法,然后返回⼀个⾼阶函数,这个⾼ 阶函数接收⼀个组件,返回⼀个⾼阶组件(其实就是给传⼊的组件增加⼀些属性和功能)connect根据传⼊的map,将 state和dispatch(action)挂载⼦组件的props上,我们直接放出connect的实现代码:
export function connect(mapStateToProps, mapDispatchToProps) {
return function(Component) {
class Connect extends React.Component {
componentDidMount() {
//从context获取store并订阅更新
this.context.store.subscribe(this.handleStoreChange.bind(this));
}
handleStoreChange() {
// 触发更新
// 触发的⽅法有多种,这⾥为了简洁起⻅,直接forceUpdate强制更新,读者也可以通过setState来触发⼦组件更新
this.forceUpdate()
}
render() {
return (
<Component
// 传⼊该组件的props,需要由connect这个⾼阶组件原样传回原组件 { ...this.props }
// 根据mapStateToProps把state挂到this.props上
{ ...mapStateToProps(this.context.store.getState()) }
// 根据mapDispatchToProps把dispatch(action)挂到this.props上
{ ...mapDispatchToProps(this.context.store.dispatch) }
/>
)
}
}
//接收context的固定写法
Connect.contextTypes = {
store: PropTypes.object
}
return Connect
}
}
写完了connect的代码,我们有两点需要解释⼀下:
-
Provider的意义:我们审视⼀下connect的代码,其实context不过是给connect提供了获取store的途径,我们在 connect中直接import store完全可以取代context。那么Provider存在的意义是什么,上⾯这个connect是⾃⼰写 的,当然可以直接import store,但react-redux的connect是封装的,对外只提供api,所以需要让Provider传⼊ store。
-
connect中的装饰器模式:回顾⼀下connect的调⽤⽅式:
connect(mapStateToProps, mapDispatchToProps)(App)其实 connect完全可以把App跟着mapStateToProps⼀起传进去,看似没必要return⼀个函数再传⼊App,为什么reactredux要这样设计,react-redux作为⼀个被⼴泛使⽤的模块,其设计肯定有它的深意。
其实connect这种设计,是装饰器模式的实现,所谓装饰器模式,简单地说就是对类的⼀个包装,动态地拓展类的功 能。connect以及React中的⾼阶组件(HoC)都是这⼀模式的实现。除此之外,也有更直接的原因:这种设计能够兼 容ES7的 装饰器(Decorator) ,使得我们可以⽤@connect这样的⽅式来简化代码。
写完了react-redux,我们可以写个demo来测试⼀下:使⽤ create-react-app 创建⼀个项⽬,删掉⽆⽤的⽂件,并创建 store.js、reducer.js、react-redux.js来分别写我们redux和react-redux的代码,index.js是项⽬的⼊⼝⽂件,在App.js中我们简单的写⼀个计数器,点击按钮就派发⼀个dispatch,让store中的count加⼀,⻚⾯上显⽰这个count。 最后⽂件⽬录和代码如下:
// store.js
export const createStore = (reducer) => {
let currentState = {}
let observers = [] //观察者队列
function getState() {
return currentState
}
function dispatch(action) {
currentState = reducer(currentState, action)
observers.forEach(fn => fn())
}
function subscribe(fn) {
observers.push(fn)
}
dispatch({ type: '@@REDUX_INIT' }) //初始化store数据
return { getState, subscribe, dispatch }
}
//reducer.js
const initialState = {
count: 0
}
export function reducer(state = initialState, action) {
switch(action.type) {
case 'plus':
return {
...state,
count: state.count + 1
}
case 'subtract':
return {
...state,
count: state.count - 1
}
default:
return initialState
}
}
//react-redux.js
import React from 'react'
import PropTypes from 'prop-types'
export class Provider extends React.Component {
// 需要声明静态属性childContextTypes来指定context对象的属性,是context的固定写法
static childContextTypes = {
store: PropTypes.object
}
// 实现getChildContext⽅法,返回context对象,也是固定写法
getChildContext() {
return { store: this.store }
}
constructor(props, context) {
super(props, context)
this.store = props.store
}
// 渲染被Provider包裹的组件
render() {
return this.props.children
}
}
export function connect(mapStateToProps, mapDispatchToProps) {
return function(Component) {
class Connect extends React.Component {
componentDidMount() {
//从context获取store并订阅更新
this.context.store.subscribe(this.handleStoreChange.bind(this));
}
handleStoreChange() {
// 触发更新
// 触发的⽅法有多种,这⾥为了简洁起⻅,直接forceUpdate强制更新,读者也可以通过setState来触发⼦组件更新
this.forceUpdate()
}
render() {
return (
<Component
// 传⼊该组件的props,需要由connect这个⾼阶组件原样传回原组件
{ ...this.props }
// 根据mapStateToProps把state挂到this.props上
{ ...mapStateToProps(this.context.store.getState()) }
// 根据mapDispatchToProps把dispatch(action)挂到this.props上
{ ...mapDispatchToProps(this.context.store.dispatch) }
/>
)
}
}
//接收context的固定写法
Connect.contextTypes = {
store: PropTypes.object
}
return Connect
}
}
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Provider } from './react-redux'
import { createStore } from './store'
import { reducer } from './reducer'
ReactDOM.render(
<Provider store={createStore(reducer)}>
<App />
</Provider>,
document.getElementById('root')
)
// App.js
import React from "react";
import { connect } from "./react-redux";
const addCountAction = { type: "plus" };
const mapStateToProps = (state) => {
return {
count: state.count
};
};
const mapDispatchToProps = (dispatch) => {
return {
addCount: () => {
dispatch(addCountAction);
}
};
};
class App extends React.Component {
render() {
return (
<div className="App">
{this.props.count}
<button onClick={() => this.props.addCount()}>增加</button>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
运⾏项⽬,点击增加按钮,能够正确的计数,OK⼤成功,我们整个redux、react-redux的流程就⾛通了。
redux Middleware 的实现
上⾯redux和react-redux的实现都⽐较简单,下⾯我们来分析实现稍困难⼀些的redux中间件。所谓中间件,我们可以 理解为拦截器,⽤于对某些过程进⾏拦截和处理,且中间件之间能够串联使⽤。在redux中, 我们中间件拦截的是dispatch提交 到reducer这个过程,从⽽增强dispatch的功能。
我们思考⼀下,如果我们想在每次dispatch之后,打印⼀下store的内容,我们会如何实现呢:
1. 在每次dispatch之后⼿动打印store的内容
store.dispatch({ type: 'plus' })
console.log('next state', store.getState())
这是最直接的⽅法,当然我们不可能在项⽬⾥每个dispatch后⾯都粘贴⼀段打印⽇志的代码,我们⾄少要把这部分功能 提取出来。
2. 封装dispatch
function dispatchAndLog(store, action) {
store.dispatch(action)
console.log('next state', store.getState())
}
我们可以重新封装⼀个公⽤的新的dispatch⽅法,这样可以减少⼀部分重复的代码。不过每次使⽤这个新的dispatch都 得从外部引⼀下,还是⽐较⿇烦。
3. 替换dispatch
let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
let result = next(action)
console.log('next state', store.getState())
return result
}
如果我们直接把dispatch给替换,这样每次使⽤的时候不就不需要再从外部引⽤⼀次了吗?对于单纯打印⽇志来说,这 样就⾜够了,但是如果我们还有⼀个监控dispatch错误的需求呢,我们固然可以在打印⽇志的代码后⾯加上捕获错误的 代码,但随着功能模块的增多,代码量会迅速膨胀,以后这个中间件就没法维护了,我们希望不同的功能是独⽴的 可拔插的模块。
4. 模块化
// 打印⽇志中间件
function patchStoreToAddLogging(store) {
let next = store.dispatch //此处也可以写成匿名函数
store.dispatch = function dispatchAndLog(action) {
let result = next(action)
console.log('next state', store.getState())
return result
}
}
// 监控错误中间件
function patchStoreToAddCrashReporting(store) {
//这⾥取到的dispatch已经是被上⼀个中间件包装过的dispatch, 从⽽实现中间件串联
let next = store.dispatch
store.dispatch = function dispatchAndReportErrors(action) {
try {
return next(action)
} catch (err) {
console.error('捕获⼀个异常!', err)
throw err
}
}
}
我们把不同功能的模块拆分成不同的⽅法,通过在⽅法内获取上⼀个中间件包装过的store.dispatch实现链式调⽤。然 后我们就能通过调⽤这些中间件⽅法,分别使⽤、组合这些中间件。
patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)
到这⾥我们基本实现了可组合、拔插的中间件,但我们仍然可以把代码再写好看⼀点。我们注意到,我们当前写的中间 件⽅法都是先获取dispatch,然后在⽅法内替换dispatch,这部分重复代码我们可以再稍微简化⼀下:我们不在⽅法内 替换dispatch,⽽是返回⼀个新的dispatch,然后让循环来进⾏每⼀步的替换。
5. applyMiddleware
改造⼀下中间件,使其返回新的dispatch⽽不是替换原dispatch。
function logger(store) {
let next = store.dispatch
// 我们之前的做法(在⽅法内直接替换dispatch):
// store.dispatch = function dispatchAndLog(action) {
// ...
// }
return function dispatchAndLog(action) {
let result = next(action)
console.log('next state', store.getState())
return result
}
}
在Redux中增加⼀个辅助⽅法applyMiddleware,⽤于添加中间件。
function applyMiddleware(store, middlewares) {
middlewares = [ ...middlewares ] //浅拷⻉数组, 避免下⾯reserve()影响原数组
middlewares.reverse() //由于循环替换dispatch时,前⾯的中间件在最⾥层,因此需要翻转数组才能保证中间件的调⽤顺序
// 循环替换dispatch
middlewares.forEach(middleware =>
store.dispatch = middleware(store)
)
}
然后我们就能以这种形式增加中间件了:
applyMiddleware(store, [ logger, crashReporter ])
写到这⾥,我们可以简单地测试⼀下中间件。我创建了三个中间件,分别是logger1、thunk、logger2,其作⽤也很简 单,打印logger1 → 执⾏异步dispatch → 打印logger2,我们通过这个例⼦观察中间件的执⾏顺序。
// index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { Provider } from "./react-redux";
import { createStore } from "./store";
import { reducer } from "./reducer";
let store = createStore(reducer);
function logger(store) {
let next = store.dispatch;
return (action) => {
console.log("logger1");
let result = next(action);
return result;
};
}
function thunk(store) {
let next = store.dispatch;
return (action) => {
console.log("thunk");
return typeof action === "function" ? action(store.dispatch) : next(action);
};
}
function logger2(store) {
let next = store.dispatch;
return (action) => {
console.log("logger2");
let result = next(action);
return result;
};
}
function applyMiddleware(store, middlewares) {
middlewares = [...middlewares];
middlewares.reverse();
middlewares.forEach((middleware) => {
store.dispatch = middleware(store);
});
}
applyMiddleware(store, [logger, thunk, logger2]);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
6. 纯函数
之前的例⼦已经基本实现我们的需求,但我们还可以进⼀步改进,上⾯这个函数看起来仍然不够“纯”,函数在函数体内 修改了store⾃⾝的dispatch,产⽣了所谓的“副作⽤”,从函数式编程的规范出发,我们可以进⾏⼀些改造,借鉴reactredux的实现思路,我们可以把applyMiddleware作为⾼阶函数,⽤于增强store,⽽不是替换dispatch:
先对createStore进⾏⼀个⼩改造,传⼊heightener(即applyMiddleware),heightener接收并强化createStore。
// store.js
export const createStore = (reducer, heightener) => {
// heightener是⼀个⾼阶函数,⽤于增强createStore
//如果存在heightener,则执⾏增强后的createStore
if (heightener) {
return heightener(createStore)(reducer)
}
let currentState = {}
let observers = [] //观察者队列
function getState() {
return currentState
}
function dispatch(action) {
currentState = reducer(currentState, action);
observers.forEach(fn => fn())
}
function subscribe(fn) {
observers.push(fn)
}
dispatch({ type: '@@REDUX_INIT' })//初始化store数据
return { getState, subscribe, dispatch }
}
中间件进⼀步柯⾥化,让next通过参数传⼊
const logger = store => next => action => {
console.log('log1')
let result = next(action)
return result
}
const thunk = store => next =>action => {
console.log('thunk')
const { dispatch, getState } = store
return typeof action === 'function' ? action(store.dispatch) : next(action)
}
const logger2 = store => next => action => {
console.log('log2')
let result = next(action)
return result
}
改造applyMiddleware
const applyMiddleware = (...middlewares) => createStore => reducer => {
const store = createStore(reducer)
let { getState, dispatch } = store
const params = {
getState,
dispatch: (action) => dispatch(action)
//解释⼀下这⾥为什么不直接 dispatch: dispatch
//因为直接使⽤dispatch会产⽣闭包,导致所有中间件都共享同⼀个dispatch,如果有中间件修改了dispatch或者进⾏异步dispatch就可能出错
}
const middlewareArr = middlewares.map(middleware => middleware(params))
dispatch = compose(...middlewareArr)(dispatch)
return { ...store, dispatch }
}
//compose这⼀步对应了middlewares.reverse(),是函数式编程⼀种常⻅的组合⽅法
function compose(...fns) {
if (fns.length === 0) return arg => arg
if (fns.length === 1) return fns[0]
return fns.reduce((res, cur) =>(...args) => res(cur(...args)))
}
代码应该不难看懂,在上⼀个例⼦的基础上,我们主要做了两个改造
- 使⽤compose⽅法取代了middlewares.reverse(),compose是函数式编程中常⽤的⼀种组合函数的⽅式, compose内部使⽤reduce巧妙地组合了中间件函数,使传⼊的中间件函数变成
(...arg) => mid1(mid2(mid3(...arg)))这 种形式。 - 不直接替换dispatch,⽽是作为⾼阶函数增强createStore,最后return的是⼀个新的store。