前言
个人认为 React 本身的学习曲线并不高,但是学习到 Redux 时,突然发现多出了好多概念。例如: Action 、 ActionCreator 、Dispatch 、Reducer 、 combineReducers。
当你学习完它们之后,突然又发现一些新东西,如 react-redux ,顾名思义它就是 redux 与 react之前的桥梁。它又包含了 Provider 组件、 connect 函数以及 mapStateToProps , mapDispatchToProps 等等细节的东西。
当你认为已经快结束时你会发现还有一个大 BOSS 在等着你,那就是 redux 发送异步请求以及中间件。
为了能讲清楚这些概念本文将分如下模块:
Redux 基础篇
redux基础概念串讲redux设计思想与核心概念
有了这些基础概念的加持,我们就可以开始使用它来完成一个小小的项目了。通过实战来加深理解。
Redux 项目实战篇
redux实现计数器- 使用
react-redux实现计数器 redux中的异步方案- 使用
redux-thunk中间件进行异步处理
这一步完成之后,概念就更加清楚了, redux 也会简单使用了。
Redux 原理篇
redux实现原理redux middleware实现原理
经历这个步骤之后,相信你就能做知其然和知其所以然。
Redux 基础篇
Redux 是什么
它就是一个状态管理容器,所有的状态,保存在一个对象里面。
为什么需要使用 Redux
Redux 绝非必需品,作者参与的很多 React 项目中都未使用 Redux 进行状态管理。
在决定是否使用 Redux 时,我们先来看看,它们管理状态对比示意图:
未使用 Redux 状态管理示意图:
使用Redux状态管理示意图:
对比下来你会发现:
- 未使用
Redux时,状态分布于各个组件自身进行管理; - 使用
Redux时,状态提升到一个全局对象中进行统一管理,不在组件中进行管理了。每个组件都可以获取到全局状态。
也就是说层级够深,状态操作够复杂的情况下,此时是可以开始考虑使用 Redux 进行状态管理。反之的话就没有必要引入而增加复杂度以及学习成本。
Redux 设计思想与核心概念
咋一看还是有点复杂,结合示意图来分块来理解它们。
Store
Store 就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store 。
import { createStore } from 'redux';
import rootReducer from "./rootReducer";
const store = createStore(rootReducer);
Store 这个全局对象会对外提供三样东西:
getState()获取全局状态的方法;dispatch(action)通过传入action调用到相应的reducer从而改变state;subscribe通过reducer改变了state值,必须要通知到组件才能进行状态更新,因此需要发布订阅中心。
Action
Action 是一个对象,用来代表所有会引起状态(state)变化的行为。
export const toggleTodo = {
type: 'TOGGLE_TODO',
id: 1
}
把它的 type 传入 dispatch 中最终决定要执行哪个 reducer 来更新相应的状态。
ActionCreator
View 要发送多少种消息,就会有多少种 Action 。如果都手写会很麻烦。可以定义一个函数来生成 Action ,这一类函数就叫 ActionCreator 。
export const toggleTodo = id => ({
type: 'TOGGLE_TODO',
id
})
不用对它有过多的解读,它就是一个函数,如它的名称一样,它的职责是生成 Action 。
dispatch()
store.dispatch() 是 View 发出 Action 的唯一方法。
store.dispatch(toggleTodo(1))
通过 toggleTodo 生成 action ,并传入 dispatch 函数中。它最终的目的就是去触发 reducer 函数执行。
Reducer
Reducer 是一个函数,它接受 Action 和当前 state 作为参数,返回一个新的 state 。
Reducer 函数最重要的特征是,它是一个纯函数。也就是说,只要是同样的输入,必定得到同样的输出。
const todos = (state = [], action) => {
switch (action.type) {
case 'TOGGLE_TODO':
return state.map(todo =>
(todo.id === action.id)
? {...todo, completed: !todo.completed}
: todo
)
default:
return state
}
}
export default todos
当发出 type = 'TOGGLE_TODO' 的 action 时,就触发了 todos 函数( reducer ) 的 'TOGGLE_TODO' 分支的代码执行。
首先遍历 state (它是当前 state ),通过判断找到传入 id 所属的那条数据。并修改属性,返回全新的 state 。
这里其实就是把如何更新 state 的逻辑写在这里。
[注意] return {...todo, completed: !todo.completed} 熟悉 ES6 语法的同学都知道这里就是做了浅拷贝,返回了一个全新的对象。而不是在原有的基础上做的修改。这个是 redux 非常重要的点,永远不要原对象上进行修改。
subscribe()
state 的数据现在是可以变更了,所以我们需要一种机制,订阅了这个数据的组件知道它变更了,并且可以获取到最新的 state ,那么就必须要实现一套发布订阅。
import { createStore } from 'redux';
const store = createStore(reducer);
store.subscribe(()=>{
// state 发生变化后,变化触发回调
});
具体实现,我们后面会讲,其实就是普通的发布订阅模式。
讲了这么多,也只是展示了一些代码片段,那么我们通过一个完整 redux 入门案例来看看,它们具体是如何工作的?
import { createStore } from "redux";
const reducer = (state = {count: 0}, action) => {
switch (action.type){
case 'INCREASE': return {count: state.count + 1};
case 'DECREASE': return {count: state.count - 1};
default: return state;
}
};
const actions = {
increase: () => ({type: 'INCREASE'}),
decrease: () => ({type: 'DECREASE'})
}
const store = createStore(reducer);
解析:
- 创建
reducer纯函数,第一个参数初始化了state为{count:0},第二个参数接收一个action; - 正常
action会包含一个type值,和payload就是传递进来的数据(这个案例过于简单并没有额外传递数据); - 创建
actions对象,对象里面就是两个ActionCreate; - 最后通过
createStore函数,传入reducer作为参数,生成了store对象。
store.subscribe(() =>
console.log(store.getState())
);
store.dispatch(actions.increase());// 触发一次增加
store.dispatch(actions.decrease());// 触发一次减少
解析:
- 首先我们订阅
store对象,这样当state发生变化之后,我们就可以得到通知; - 我们通过
store.dispatch的方式可以发出各种action,可以理解发出各种命令,表示我要做的事情。
它的工作流大概如下:
- 首先,用户发出
Action,发出方式是通过dispatch方法; - 然后,
Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State; State一旦有变化,Store就会调用监听函数。
到这里您应该明白,关于 redux 是如何工作的。
那么我们接下来的实际案例中就来看看结合 React 它又是怎么样的一种工作方式呢?
实战应用篇
实战应用篇分为4个小例子:
1、react 中只使用 redux 实现一个计数器 2、react 中借助 react-redux 实现一个计数器
通过这两个案例的对比,理解为什么需要 react-redux
3、不借助任何中间件在 React 中发起异步请求
4、借助 redux-thunk 发起异步请求
通过以上两个案例,首先让我们有了中间件的概念并学会如何去使用中间件。 最后在原理篇也会详细讲解中间件的实现原理。
redux 计数器
为了演示代码的便捷,作者尽量把组件写在一个文件中,不进行拆分。
首先 create-react-app xxx 快速创建项目。
index.js 代码:
import React,{useState} from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
// {1}
const reducer = (state = {count: 0}, action) => {
switch (action.type){
case 'INCREASE': return {count: state.count + 1};
case 'DECREASE': return {count: state.count - 1};
default: return state;
}
};
// {2}
const actions = {
increase: () => ({type: 'INCREASE'}),
decrease: () => ({type: 'DECREASE'})
}
// {3}
const store = createStore(reducer);
function App() {
const { getState, dispatch, subscribe } = store; // {4}
const [ count, setCount ] = useState(getState().count); // {5}
// {6}
const onIncrement = ()=>{
dispatch(actions.increase());
}
// {7}
const onDecrement = ()=>{
dispatch(actions.decrease());
}
// {8}
subscribe(()=>{
setCount(getState().count);
});
return (
<div>
<h1>{count}</h1>
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
</div>
);
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
解析:
- {1} {2} {3} 通过
createStore创建了一个全局store对象; - {4} 在业务组件
App中,获取store提供给我们的所有能力; - {5} 通过
useStateHook初始化一个count状态,初始化的值则是通过store.getState().count获取; - {6} {7} 创建两个
dispatch方法; - {8} 重点在这里,用户在页面发出点击事件后,触发了相应的
dispatch方法,执行了相应的reducer方法,修改了状态,这里进行监听获取最新的状态,通过setCount把最新的状态更新到界面上。
这样我们就完成了一个非常简单的应用了,代码托管地址 >>>请点击。
在这个例子中,我们通过引入全局 store 对象并直接使用,假设现在业务更加繁琐,我们有很多子组件,孙子组件,那么我们可能就需要每个组件都这样手动引入 store 了。
import { store,actions } from "./index.js";
你不能说这样不行,只能说不够优雅便捷,学习过 React 的同学当碰到这样的需求时,第一个想到的应该是 context 。因此官方提供了这么个工具 react-redux 从名字上可以看出来,它是 react 与 redux 的桥梁。下面我们来具体看看它的应用逻辑。
react-redux 计数器
我们通过 react-redux 实现上面那个计数器示例。
项目目录结构
├── src
├── actions.js // 编写 action 文件
├── reducer.js // 编写 reducer 文件
├── index.js // 起始文件
├── Count.js // 负责UI渲染的组件
└── CountContainer.js // 容器组件负责管理数据和业务逻辑
actions.js
export const actions = {
increase: () => ({type: 'INCREASE'}),
decrease: () => ({type: 'DECREASE'})
}
reducer.js
const counter = (state = { count: 0 }, action) =>{
switch (action.type){
case 'INCREASE': return {count: state.count + 1};
case 'DECREASE': return {count: state.count - 1};
default: return state;
}
}
export default counter;
正常的 reducer 纯函数,在项目中,常常会使用 combineReducers 对多个 reducer 进行合并,它并不是本文的主角了解即可。
Count.js
function Count(props){
const { count, onIncrement, onDecrement } = props;
return (
<div>
<h1>{count}</h1>
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
</div>
)
}
CountContainer.js
import { actions } from "./actions";
import { connect } from 'react-redux'
import Count from "./Count";
function mapStateToProps(state) {
return {
count: state.count
}
}
function mapDispatchToProps(dispatch) {
return {
onIncrement: () => dispatch(actions.increase()),
onDecrement: ()=> dispatch(actions.decrease())
}
}
export const CountContainer = connect(
mapStateToProps,
mapDispatchToProps
)(Count)
想必 react-redux 的核心都在这个 container 组件中了,纵观整个组件你会发现,它竟然没有 render 任何元素出去,而仅仅是对 Count 组件进行包裹,返回一个新的组件。
从 Count 组件多了那3个属性,不难发现,正是 mapStateToProps 与 mapDispatchToProps 所返回的值,通过 connect 函数绑定到 Count 组件中。
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { CountContainer } from "./CountContainer";
import reducers from "./reducer";
const store = createStore(reducers);
ReactDOM.render(
<Provider store={store}>
<CountContainer />
</Provider>,
document.getElementById('root')
)
在 root 级文件中,我们做了两个事情:
- 创建了全局唯一的
store对象; - 传入为
Provder组件的属性。
最终运行效果,同上一个例子一致。但是它是怎么做到的呢?我们来一一分析它的原理。
connect
connect(mapStateToProps, mapDispatchToProps)(App)
connect 接收 mapStateToProps 、 mapDispatchToProps 两个方法,返回一个高阶函数,这个高阶函数接收一个组件,返回一个新组件(其实就是给传入的组件增加一些属性和功能)。
简化版 connect 内部实现:
import React from 'react'
import { Context } from "./Provider";
export function connect(mapStateToProps, mapDispatchToProps) {
return function(Component) {
class Connect extends React.Component {
componentDidMount() {
// 订阅全局state,当其发生变化时,更新所有组件。
this.context.subscribe(()=>this.forceUpdate());
}
render() {
return (
<Component
{/* 透传props */}
{ ...this.props }
{/* 调用mapStateToProps,返回值作为属性传入 */}
{ ...mapStateToProps(this.context.getState()) }
{/* 调用mapDispatchToProps,返回值作为属性传入 */}
{ ...mapDispatchToProps(this.context.dispatch) }
/>
)
}
}
Connect.contextType = Context;
return Connect
}
}
结合 connect 源码再来理解它的写法:
export const CountContainer = connect(
mapStateToProps,
mapDispatchToProps
)(Count)
是不是就豁然开朗了呢?
Provider 组件
它就相对比较简单了,它的作用就是获取 store 对象并存入到 context 中。我们来看看它简化版的实现:
import React from 'react';
export const Context = React.createContext(null);
function Provider({ store, children }) {
return <Context.Provider value={store}>{children}</Context.Provider>
}
想必看到这里你应该能彻底理解 react-redux 是如何做到 react 与 redux 的桥梁。
细心的你肯定会发现,上述所有案例都是同步的,但在实际项目中,我们触发增加或者减少按钮,往往都是需要把状态存储到数据库中,因此就必须要调用后台接口,这个就是我们熟悉的异步过程。接下来我们看看如何在组件中发起异步请求。
不借助中间件的异步请求
思考一个简单的场景,点击获取数据按钮请求后台接口返回列表,请求之初要展示loading状态,请求成功之后展示列表,如果请求失败则提示失败信息。
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {App} from './App';
import { createStore } from "redux";
import { Provider } from 'react-redux'
// {1}
export const actionTypes = {
FETCH_START:'FETCH_START',
FETCH_SUCCESS:'FETCH_SUCCESS',
FETCH_ERROR:'FETCH_ERROR'
}
// {2}
const initState = {
isFetching: false, // 控制 loading 状态
newsList:[], // 新闻列表
errorMsg:"" // 错误提示信息
}
// {3}
const reducer = (state= initState,action)=>{
switch (action.type) {
case actionTypes.FETCH_START:
return {...state,isFetching:true};
case actionTypes.FETCH_SUCCESS:
return {...state,isFetching:false,newsList:action.news};
case actionTypes.FETCH_ERROR:
return {...state,isFetching:false,newsList:[],errorMsg:'服务异常'};
default:
return state;
}
}
// {4}
const store = createStore(reducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
解析:
- {1}
actionTypes很好理解,一个包含多个action的对象; - {2} 初始化
state值; - {3}
reducer纯函数,接收初始化状态和action,返回新状态; - {4} 通过
reducer创建全局store,并通过Provider组件放入context中,供子孙组件调用。
App.js
import { connect } from 'react-redux'
import News from "./News";
import {actionTypes} from "./index";
// {1}
function mapStateToProps(state) {
return state
}
// {2}
function mapDispatchToProps(dispatch) {
return {
fetchNews:()=>{
dispatch({type:actionTypes.FETCH_START}) // {3}
// {4}
new Promise(function (resolve,reject) {
setTimeout(()=>{
resolve([{title:"111"},{title:"222"}]);
},2000)
}).then((response)=>{
// {5}
dispatch({
type:actionTypes.FETCH_SUCCESS,
news:response
})
}).catch(()=>{
// {6}
dispatch({
type:actionTypes.FETCH_ERROR
})
})
}
}
}
export const App = connect(
mapStateToProps,
mapDispatchToProps
)(News)
解析:
- {1}
mapStateToProps上面已经提及了它的作用,将state映射到输出组件的props; - {2}
mapDispatchToProps将dispatch映射到输出组件的props; - {3} 发出
FETCH_START的action,改变state中isFetching的状态为true,表示请求开始; - {4} 权当这个
Promise是向后台请求的一个异步方法; - {5} 请求成功后发出
FETCH_SUCCESS的action,并且把最新的数据列表传入; - {6} 如果请求过程中发生了错误就发出
FETCH_ERROR的action。
接下来看最后一个组件 News.js
import React from 'react';
function News({isFetching,newsList,errorMsg,fetchNews}) {
return (
<div className="news">
{/* 1 */}
<button onClick={fetchNews}>获取新闻列表</button>
{
isFetching ?
<div>数据加载中...</div> :
(
<ul>
{
newsList.map((item,index)=>{
return <li key={index}>{item.title}</li>
})
}
</ul>
)
}
</div>
);
}
export default News;
解析:
- {1} 这是一个纯 UI 组件,当点击按钮的时候执行
fetchNews函数,其实就相当于执行了多个dispatch方法去改变状态。
平时开发你完全可以这样去处理异步问题,但其实 redux 提供了中间件给我们使用。通过使用中间件去处理异步问题,也让团队有规范可依。
常用处理异步有: redux-thunk 、 redux-saga 等中间件。但这不是本文的重点,重点是通过使用中间件,最后让我们去理解整个中间件系统是如何工作的,因此今天我们只讲解 redux-thunk 的使用,理由是它简单。
使用redux-thunk中间件的异步请求
还是使用上面的例子,改动也不大。来看下代码:
import thunk from 'redux-thunk'
export const actionTypes = {
... 同上一致
}
// {1}
export const actionCreator = {
fetchNews:()=>{
return (dispatch)=>{
dispatch({type:actionTypes.FETCH_START})
new Promise(function (resolve,reject) {
setTimeout(()=>{
resolve([{title:"111"},{title:"222"}]);
},2000)
}).then((response)=>{
dispatch({
type:actionTypes.FETCH_SUCCESS,
news:response
})
}).catch(()=>{
dispatch({
type:actionTypes.FETCH_ERROR
})
})
}
}
}
const initState = {
... 同上一致
}
const reducer = (state= initState,action)=>{
... 同上一致
}
// {2}
const store = createStore(reducer,applyMiddleware(thunk));
解析:
- {1}
actionCreator对象里面包含fetchNews,整个函数体与上面的示例也是一致的,唯一不一样的地方就是,return返回是一个函数; - {2}
createStore(reducer,applyMiddleware(thunk))创建store的时候传入thunk中间件。
App.js
import {actionCreator} from "./index";
function mapDispatchToProps(dispatch) {
return {
fetchNews:()=>{
dispatch(actionCreator.fetchNews());
}
}
}
解析:
- 这里新创建一个
fetchNews方法,里面dispatch调用了actionCreator.fetchNews()返回的函数。其实前面在讲解dispatch的时候它都只能执行一个对象形如{type:'FETCH_SUCCESS'},但是这里却执行的是一个函数dispatch((dispatch)=>{...}),原因就是使用了redux-thunk。
其它代码都一致,到这里就改造完成 >>> 点击查阅完成代码。
当然还有很多很多优秀的中间件可以帮助我们快速完成项目例如: redux-saga 、 redux-promise 。
但是本文的重点是搞清楚 redux 以及它的中间件原理,接下来让我们一起来看看 redux 它的实现原理是怎么样的?
实现原理
redux 实现原理
回顾下上面的使用过程,通过 createStore 方法创建出 store 对象,该对象包含了 getStat() 、 dispatch() 、 subscribe() ,因此它的初步结构应该是这样的:
export const createStore = () => {
let currentState = {}
function getState() {}
function dispatch() {}
function subscribe() {}
return { getState, dispatch, subscribe }
}
getState
获取到state值
function getState() {
return currentState;
}
dispatch
回顾上面的使用 dispatch({type:'FETCH_SUCCESS'}) ,它是一个函数,参数就是一个 action ,其实本质是通过执行的 reducer 函数实现状态的改变:
function dispatch(action) {
// reducer 执行完之后会返回一个新的状态。
currentState = reducer(currentState,action);
}
dispatch({ type: "INIT" }) // {1}
为什么需要先 dispatch 执行一次?我把上个示例的代码贴出来你就明白了:
const initState = {
isFetching: false,
newsList:[],
errorMsg:""
}
const reducer = (state= initState,action)=>{
switch (action.type) {
... 省略
default:
return state;
}
}
我们定义的 initState 的状态,在 redux 中是不知道的,因此先调用一次 dispatch({ type: INIT }) 则会去执行 reducer 方法的 default 分支,返回 initState 初始状态。
function dispatch(action) {
currentState = reducer(currentState,action);
}
# 执行后就等于
dispatch({ type: "INIT" })
currentState = {
isFetching: false,
newsList:[],
errorMsg:""
}
subscribe
现在已经可以改变状态了,现在只需要一套发布订阅的代码就可以了
import { reducer } from './reducer'
export const createStore = (reducer) => {
let currentState = {}
let observers = [] // {1} 订阅队列
function dispatch(action) {
currentState = reducer(currentState, action)
// {3} 当状态发生了变更就执行所有订阅方法
observers.forEach(fn => fn())
}
// {2} 把订阅的方法塞入队列中
function subscribe(fn) {
observers.push(fn)
}
}
到这里就有了不包含中间件系统的 redux 的版本:
import { reducer } from './reducer'
export const createStore = (reducer) => {
let currentState = {}
const observers = []
function getState() {
return currentState
}
function dispatch(action) {
currentState = reducer(currentState, action)
observers.forEach(fn => fn())
}
function subscribe(fn) {
observers.push(fn)
}
dispatch({ type: 'INIT' })
return { getState, subscribe, dispatch }
}
这个版本 redux 还是非常简单容易理解的,接下来分析中间件实现原理就会有点难度咯。
redux 中间件实现原理
中间件(英语:Middleware),是提供系统软件和应用软件之间连接的软件,以便于软件各部件之间的沟通,特别是应用软件对于系统软件的集中的逻辑,在现代信息技术应用框架如Web服务、面向服务的体系结构等中应用比较广泛。如数据库、Apache的Tomcat,IBM公司的WebSphere,BEA公司的WebLogic应用服务器,东方通公司的Tong系列中间件,以及Kingdee公司的等都属于中间件。
它本身是一种软件架构思想,在前端应用中也是相当广泛,例如 koa 、 express 以及本文的主题 redux 都实现了一套自己的中间件系统。而这种实现带来了一个产物(结果)就是洋葱模型。
思考一个简单的例子:在视图中发出一个 dispatch({type:"T1"}) 后,想打印日志该如何做?
store.dispatch({ type: 'T1' })
console.log('输出日志1');
正常的思维逻辑就是直接写在后面即可。类似这样的需求会越来越多,这里就假设希望输出3种类型的日志。
store.dispatch({ type: 'T1' })
console.log('输出日志1');
console.log('输出日志2');
console.log('输出日志3');
但其实输出日志的需求,在各个项目中都是有的?有没有什么办法可以这个事情变得更加简单呢?
例如:
store.dispatch({ type: 'T1' })
showLog1(store)
showLog2(store)
showLog3(store)
function showLog1(store){
console.log('输出日志1', store.getState());
}
function showLog2(store){
console.log('输出日志2', store.getState());
}
function showLog3(store){
console.log('输出日志3', store.getState());
}
把它封装在函数中,调用起来就更加方便了。而且也可以实现复用。
此时需求再次发生变化,我不仅希望在执行 store.dispatch({ type: 'T1' }) 的时候打出日志,在执行T2、T3...等等都希望打出日志来。
execuT1();
execuT2();
execuT3();
function execuT1(){
store.dispatch({ type: 'T1' })
showLog1(store)
showLog2(store)
showLog3(store)
}
function execuT2(){
store.dispatch({ type: 'T2' })
showLog1(store)
showLog2(store)
showLog3(store)
}
function execuT3(){
store.dispatch({ type: 'T3' })
showLog1(store)
showLog2(store)
showLog3(store)
}
对这样做就可以了,这样以后不论多少种 dispatch 都会打印这3中日志了。但是这样显然是不合理的。并且写了非常多的重复代码。
现在我们已经渐渐明白 showLog1、showLog2、showLog3 其实就是中间件。它的本质也就是一个一个函数。
现在我们希望有一套中间件系统,它具备两个功能:
- 可以快速执行这些中间件
- 并且实现中间件之间信息传递
我们先对第一点进行代码实施:
let store = createStore(reducer)
function applyMiddleware(store, middlewares) {
middlewares = [ ...middlewares ]
middlewares.forEach(middleware =>
middleware(store)
)
}
applyMiddleware(store, [ showLog1, showLog2, showLog3 ])
现在这个中间件系统 applyMiddleware 通过传参可以快速执行注册好的中间件。但是这里有个问题,这个系统会在整个应用初始化的时候就快速执行所有中间件。而我们所期望的是,当执行 dispatch 函数时,才去执行它们。
现在视图层调用的 dispatch 方法是这样的:
function dispatch(action) {
currentState = reducer(currentState, action)
observers.forEach(fn => fn())
}
该 dispatch 只会去触发 reducer 方法的执行,如果我们希望执行 dispatch 的时候,会按照中间件注册顺序,执行一遍中间件?
现在假设我们去修改 dispatch 的行为如下:
newDispatch = showLog1(showLog2(showLog3(oldDispatch)));
这样我们在视图层调用任意 dispatch 时,就相当于调用了 newDispatch 这个函数。
这样既把所有中间件串联起来,而且也让中间件之间能“消息转换”,毕竟它们是在同一个作用域下。
反观我们的中间件的格式:
function showLog1(store){
console.log('输出日志1', store.getState());
}
它的结构过于简单是无法满足我们当前的需求,我们需要对它进行改造:
function showLog1(store) {
let next = store.dispatch
return (action) => {
console.log('进入日志1');
const result = next(action)
console.log('退出日志1');
return result
}
}
function showLog2(store) {
let next = store.dispatch
return (action) => {
console.log('进入日志2');
const result = next(action)
console.log('退出日志2');
return result
}
}
function showLog3(store) {
let next = store.dispatch
return (action) => {
console.log('进入日志3');
const result = next(action)
console.log('退出日志3');
return result
}
}
套用这个公式:newDispatch = showLog1(showLog2(showLog3(oldDispatch)));
function showLog1(store) {
let next = store.dispatch
return (action) => {
console.log('进入日志1');
(action) => {
console.log('进入日志2');
(action) => {
console.log('进入日志3');
oldDispatch(action);
console.log('退出日志3');
}
console.log('退出日志2',);
}
console.log('退出日志1');
}
}
如果是这样就完全可以满足我们的需求了,现在问题就是如何让每个 showLog 中的 next 保存的是下个中间件的函数调用呢?我们来改造下中间件:
function applyMiddleware(store, middlewares) {
middlewares = [ ...middlewares ]
middlewares.reverse()
middlewares.forEach(middleware =>
store.dispatch = middleware(store)
)
return store;
}
我们来分析下它的执行:
[showLog3,showLog2,showLog1].forEach(middleware =>
store.dispatch = middleware(store)
)
第一次调用:store.dispatch = showLog3(store); // 此时入参中 store.dispatch 为最原始的 dispatch
第二次调用:store.dispatch = showLog2(store); // 此时 store.dispatch 已经为 showLog3(store)
第三次调用:store.dispatch = showLog1(store); // 此时 store.dispatch 已经为 showLog2(store)
# 通过闭包的特性,最后每个 showLog 函数中的 next 都保存了下一个中间件的方法
# 在 view 层的 disaptch 实际上边成这样了:
(action) => {
console.log('进入日志1');
((action) => {
console.log('进入日志2',);
((action) => {
console.log('进入日志3');
((action) => {
currentState = reducer(currentState,action);
observers.forEach(fn=>fn());
})(action)
console.log('退出日志3');
})(action)
console.log('退出日志2',);
}(action)
console.log('退出日志1');
}
用户只要执行 dispatch 就相当于执行上面这一整个方法,所有中间件都按照顺序执行了。
目前这套中间件系统就有一个初步模型了:
# 中间件的标准格式:
function showLog1(store) {
let next = store.dispatch
return (action) => {
console.log('进入日志1');
const result = next(action);
console.log('退出日志1');
return result
}
}
# 中间件处理函数
function applyMiddleware(store, middlewares) {
middlewares = [ ...middlewares ]
middlewares.reverse()
middlewares.forEach(middleware =>
store.dispatch = middleware(store)
)
}
# 执行调用
applyMiddleware(store, [ showLog1, showLog2, showLog3 ])
这已经是一版完全可执行的中间件系统了。
而且这套中间件的执行,带来了一个有趣的现象,那就是洋葱模型。
使用图形表示如下:
增强型中间件系统
在使用过程中,我们发现使用自己的中间件系统就只能使用自己手写的中间件,没有办法使用第三方的中间件。那是因为格式不一致导致的:
# redux 中间件规范格式:
const showLog1 = store => next => action => {
console.log('进入日志1')
let result = next(action)
console.log('退出日志1')
return result
}
把下个中间件的信息 next 当做参数传入。
而且在使用标准 redux 创建中间件都是这样做的:
const store = createStore(reducer,applyMiddleware(logger,logger2,logger3));
还是调用的 createStore 方法,那我们就对它进行改造改造:
export const createStore = (reducer,applyMiddleware)=>{
if(applyMiddleware){
return applyMiddleware(createStore)(reducer);
}
... 省略
}
解析:判断是否有中间件函数的入参,如果有的话,通过中间件函数进行创建 store 。
再来看看中间件函数的改造:
export const applyMiddleware = (...middlewares) => createStore => reducer => {
const store = createStore(reducer) // 通过 createStore 创建 store
let { getState, dispatch } = store // 去除 store 的 getState 与 dispatch
// 重新拼凑一个新的参数,作为中间件的store入参
const params = {
getState,
dispatch: (action) => dispatch(action)
}
// 这个步骤是把params传入每个中间件,获取新的中间件列表。
const middlewareArr = middlewares.map(middleware => middleware(params))
// 它的作用就等同于 middlewares.reverse().forEach(middleware => store.dispatch = middleware(store))
dispatch = compose(...middlewareArr)(dispatch);
return { ...store, dispatch }
}
function compose(...fns) {
if (fns.length === 0) return arg => arg
if (fns.length === 1) return fns[0]
return fns.reduce( (res, cur) => {
return (...args) => {
return res(cur(...args))
}
})
}
我们来分析下 compose 的执行:
fns = [showLog1,showLog2,showLog3];res表示上次的运算结果,cur表示当前的入参;args第一次执行则是compose(...middlewareArr)(dispatch)中的初始dispatch;return res(cur(...args))很明显最后就会出现如下的执行结果:
middleware1(middleware2(middleware3(dispatch)));
因此它最后的效果与基础版的中间件其实是相同的。只是换了一个更加高级的写法。函数柯里化更加彻底。
想必讲到这里对于 redux 中间件系统应该能理解了,如果还是有问题那就自己动手跑跑代码,会理解的更加深刻了。这个还是需要掌握的,毕竟中间件模式在 node.js 的 express 、 koa2 等框架都有广泛的应用。
遗漏的 redux-thunk
前面的中间件都是列举 showLog1 、 showLog2 这样的简单例子,这样做的目的是更加单纯的讲解中间件系统,毕竟再添加上异步中间件的话,理解起来就更加费劲。
这里就不卖关子了,毕竟 redux-thunk 实现还是非常简单且巧妙的。
const thunk = store => next =>action => {
return typeof action === 'function' ? action(store.dispatch) : next(action)
}
如果 action 传入是函数的话,则执行 action(store.dispatch) ,等于执行下面这个函数:
// 这就是上面实战篇的应用案例
(dispatch)=>{
dispatch({type:actionTypes.FETCH_START})
new Promise(function (resolve,reject) {
setTimeout(()=>{
resolve([{title:"111"},{title:"222"}]);
},2000)
}).then((response)=>{
dispatch({
type:actionTypes.FETCH_SUCCESS,
news:response
})
}).catch(()=>{
dispatch({
type:actionTypes.FETCH_ERROR
})
})
}
该函数执行后,等于去执行函数体里面的各种 dispatch 以及异步方法里面的 dispatch 。
总结
在最开始的基础篇中,我们去理解了 action 、 dispatch 、 reducer 等等的概念,然后通过两个实战案例分别讲解了, redux 的同步以及异步的应用。最后针对 redux 的实现原理也进行了深入的学习。
相信通过阅读完本文,你会更加彻底的理解 redux 。
如果喜欢请点个赞吧!