Redux 应用与原理

1,185 阅读22分钟

前言

个人认为 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 状态管理示意图:

image.png

使用Redux状态管理示意图:

image.png

对比下来你会发现:

  • 未使用 Redux 时,状态分布于各个组件自身进行管理;
  • 使用 Redux 时,状态提升到一个全局对象中进行统一管理,不在组件中进行管理了。每个组件都可以获取到全局状态。

也就是说层级够深,状态操作够复杂的情况下,此时是可以开始考虑使用 Redux 进行状态管理。反之的话就没有必要引入而增加复杂度以及学习成本。

Redux 设计思想与核心概念

redux.jpg

咋一看还是有点复杂,结合示意图来分块来理解它们。

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 ,可以理解发出各种命令,表示我要做的事情。

它的工作流大概如下:

  1. 首先,用户发出 Action ,发出方式是通过 dispatch 方法;
  2. 然后, Store 自动调用 Reducer ,并且传入两个参数:当前 State 和收到的 Action , Reducer会返回新的 State ;
  3. 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 级文件中,我们做了两个事情:

  1. 创建了全局唯一的 store  对象;
  2. 传入为 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状态,请求成功之后展示列表,如果请求失败则提示失败信息。

代码托管地址 >>> 点击查看

QQ20201021-070541.gif

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} mapDispatchToPropsdispatch 映射到输出组件的 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中日志了。但是这样显然是不合理的。并且写了非常多的重复代码。

现在我们已经渐渐明白 showLog1showLog2showLog3 其实就是中间件。它的本质也就是一个一个函数。

现在我们希望有一套中间件系统,它具备两个功能:

  1. 可以快速执行这些中间件
  2. 并且实现中间件之间信息传递

我们先对第一点进行代码实施:

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 ])

这已经是一版完全可执行的中间件系统了。

而且这套中间件的执行,带来了一个有趣的现象,那就是洋葱模型。

image.png

使用图形表示如下: image.png

增强型中间件系统

在使用过程中,我们发现使用自己的中间件系统就只能使用自己手写的中间件,没有办法使用第三方的中间件。那是因为格式不一致导致的:

# 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 。

如果喜欢请点个赞吧!