前言
个人认为 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} 通过
useState
Hook
初始化一个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
。
如果喜欢请点个赞吧!