redux源码解读

Redux API 总览

浅谈redux 中间件的原理

原文

在 Redux 的源码目录 src/,我们可以看到如下文件结构:

├── utils/
│     ├── warning.js # 打酱油的,负责在控制台显示警告信息
├── applyMiddleware.js
├── bindActionCreators.js
├── combineReducers.js
├── compose.js
├── createStore.js
├── index.js # 入口文件

除去打酱油的 utils/warning.js 以及入口文件 index.js,剩下那 5 个就是 Redux 的 API

compose(...functions)

先说这个 API 的原因是它没有依赖,是一个纯函数

⊙ 源码分析

/**
 * 看起来逼格很高,实际运用其实是这样子的:
 * compose(f, g, h)(...arg) => f(g(h(...args)))
 *
 * 值得注意的是,它用到了 reduceRight,因此执行顺序是从右到左
 *
 * @param  {多个函数,用逗号隔开}
 * @return {函数}
 */

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  const last = funcs[funcs.length - 1]
  const rest = funcs.slice(0, -1)
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}
---
compose(fn1,fn2,fn3)(0)
![屏幕快照 2018-08-05 上午8.03.37.png](https://upload-images.jianshu.io/upload_images/8805811-1d9dd6013da732f6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

这里的关键点在于,reduceRight 可传入初始值:

// 由于 reduce / reduceRight 仅仅是方向的不同,因此下面用 reduce 说明即可
var arr = [1, 2, 3, 4, 5]

var re1 = arr.reduce(function(total, i) {
  return total + i
})
console.log(re1) // 15

var re2 = arr.reduce(function(total, i) {
  return total + i
}, 100) // <---------------传入一个初始值
console.log(re2) // 115

下面是 compose 的实例(在线演示):

<!DOCTYPE html>
<html>
<head>
  <script src="//cdn.bootcss.com/redux/3.5.2/redux.min.js"></script>
</head>
<body>
<script>
function func1(num) {
  console.log('func1 获得参数 ' + num);
  return num + 1;
}

function func2(num) {
  console.log('func2 获得参数 ' + num);
  return num + 2;
}

function func3(num) {
  console.log('func3 获得参数 ' + num);
  return num + 3;
}

// 有点难看(如果函数名再长一点,那屏幕就不够宽了)
var re1 = func3(func2(func1(0)));
console.log('re1:' + re1);

console.log('===============');

// 很优雅
var re2 = Redux.compose(func3, func2, func1)(0);
console.log('re2:' + re2);
</script>
</body>
</html>

控制台输出:

func1 获得参数 0
func2 获得参数 1
func3 获得参数 3
re1:6
===============
func1 获得参数 0
func2 获得参数 1
func3 获得参数 3
re2:6

Store Enhancer

说白了,Store 增强器就是对生成的 store API 进行改造,这是它与中间件最大的区别(中间件不修改 store 的 API)
而改造 store 的 API 就要从它的缔造者 createStore 入手。例如,Redux 的 API applyMiddleware 就是一个 Store 增强器。

为什么 applyMiddleware 要为 dispatch 创建一个闭包?

applyMiddleware 从 store 中获取已有的 dispatch,然后把它封装在一个闭包中来创建最开始的 middleware 链。然后用一个对象调用来调用,以暴露出 getState 和 dispatch 函数。这样做可以使得 middleware 在初始化时可以使用 dispatch

import compose from './compose' // 这货的作用其实就是 compose(f, g, h)(action) => f(g(h(action)))

/* 传入一坨中间件 */
export default function applyMiddleware(...middlewares) {

  /* 传入 createStore */
  return function(createStore) {

    /* 返回一个函数签名跟 createStore 一模一样的函数,亦即返回的是一个增强版的 createStore */
    return function(reducer, preloadedState, enhancer) {

      // 用原 createStore 先生成一个 store,其包含 getState / dispatch / subscribe / replaceReducer 四个 API
      var store = createStore(reducer, preloadedState, enhancer)

      var dispatch = store.dispatch // 指向原 dispatch
      var chain = [] // 存储中间件的数组

      // 提供给中间件的 API(其实都是 store 的 API)
      var middlewareAPI = {
        getState: store.getState,
        dispatch: (action) => dispatch(action)
      }

      // 给中间件“装上” API,见上面 ⊙Middleware【降低逼格写法】的【锚点-1】 
      chain = middlewares.map(middleware => middleware(middlewareAPI))
//chain数组里面的数据结构是这样的 :
//[ function(next){return function(action){...}},function(next){return function(action){...}} ...]
      // 串联所有中间件
      // store.dispatch传给last(...args)
      dispatch = compose(...chain)(store.dispatch)
      // 例如,chain 为 [M3, M2, M1],而 compose 是从右到左进行“包裹”的
      // 那么,M1 的 dispatch 参数为 store.dispatch(见【降低逼格写法】的【锚点-2】)
      // 往后,M2 的 dispatch 参数为 M1 的中间件处理逻辑哥(见【降低逼格写法】的【锚点-3】)
      // 同样,M3 的 dispatch 参数为 M2 的中间件处理逻辑哥
      // 最后,我们得到串联后的中间件链:M3(M2(M1(store.dispatch)))
      //(这种形式的串联类似于洋葱,可参考文末的拓展阅读:中间件的洋葱模型)
      // 在此衷心感谢 @ibufu 在 issue8 中指出之前我对此处的错误解读

      return {
        ...store, // store 的 API 中保留 getState / subsribe / replaceReducer
        dispatch  // 新 dispatch 覆盖原 dispatch,往后调用 dispatch 就会触发 chain 内的中间件链式串联执行
      }
    }
  }
}

最终返回的虽然还是 store 的那四个 API,但其中的 dispatch 函数的功能被增强了,这就是所谓的 Store Enhancer


Middleware

使用包含自定义功能的 middleware 来扩展 Redux 是一种推荐的方式。Middleware 可以让你包装 store 的 dispatch 方法来达到你想要的目的。同时, middleware 还拥有“可组合”这一关键特性。多个 middleware 可以被组合到一起使用,形成 middleware 链。其中,每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。

Middleware 最常见的使用场景是无需引用大量代码或依赖类似 Rx 的第三方库实现异步 actions。这种方式可以让你像 dispatch 一般的 actions 那样 dispatch 异步 actions

例如,redux-thunk 支持 dispatch function,以此让 action creator 控制反转。被 dispatch 的 function 会接收 dispatch 作为参数,并且可以异步调用它。这类的 function 就称为 thunk。另一个 middleware 的示例是 redux-promise。它支持 dispatch 一个异步的 Promise action,并且在 Promise resolve 后可以 dispatch 一个普通的 action。

Middleware 并不需要和 createStore 绑在一起使用,也不是 Redux 架构的基础组成部分,但它带来的益处让我们认为有必要在 Redux 核心中包含对它的支持。因此,虽然不同的 middleware 可能在易用性和用法上有所不同,它仍被作为扩展 dispatch 的唯一标准的方式。

说白了,Redux 引入中间件机制,其实就是为了在 dispatch 前后,统一“做爱做的事”。。。
诸如统一的日志记录、引入 thunk 统一处理异步 Action Creator 等都属于中间件
下面是一个简单的打印动作前后 state 的中间件:
让 middleware 以方法参数的形式接收一个 next() 方法,而不是通过 store 的实例去获取。

/* 装逼写法 */
const printStateMiddleware = ({ getState }) => next => action => {
  console.log('state before dispatch', getState())
  
  let returnValue = next(action)

  console.log('state after dispatch', getState())

  return returnValue
}

-------------------------------------------------

/* 降低逼格写法 */
function printStateMiddleware(middlewareAPI) { // 记为【锚点-1】,中间件内可用的 API
  return function (dispatch) {                 // 记为【锚点-2】,传入上级中间件处理逻辑(若无则为原 store.dispatch)

    // 下面记为【锚点-3】,整个函数将会被传到下级中间件(如果有的话)作为它的 dispatch 参数
    return function (action) { // <---------------------------------------------- 这货就叫做【中间件处理逻辑哥】吧
      console.log('state before dispatch', middlewareAPI.getState())
  
      var returnValue = dispatch(action) // 还记得吗,dispatch 的返回值其实还是 action
  
      console.log('state after dispatch', middlewareAPI.getState())

      return returnValue // 将 action 返回给上一个中间件(实际上可以返回任意值,或不返回)
      // 在此衷心感谢 @zaleGZL 在 issue15 中指出之前我对此处的错误解读
    }
  }
}

======
// 简单写法,主要看流程
function middleware({dispatch, getState}) {
    return function (next) {
        return function (action) {
            return next(action);
        }
    }
}

Middleware 接收了一个 next() 的 dispatch 函数,并返回一个 dispatch 函数,返回的函数会被作为下一个 middleware 的 next(),以此类推。由于 store 中类似 getState() 的方法依旧非常有用,我们将 store 作为顶层的参数,使得它可以在所有 middleware 中被使用。

参数

  • ...middleware (arguments): 遵循 Redux middleware API 的函数。每个 middleware 接受 StoredispatchgetState 函数作为命名参数,并返回一个函数。该函数会被传入 被称为 next 的下一个 middleware 的 dispatch 方法,并返回一个接收 action 的新函数,这个函数可以直接调用 next(action),或者在其他需要的时刻调用,甚至根本不去调用它。调用链中最后一个 middleware 会接受真实的 store 的 dispatch 方法作为 next 参数,并借此结束调用链。所以,middleware 的函数签名是 ({ getState, dispatch }) => next => action

返回值

(Function) 一个应用了 middleware 后的 store enhancer。这个 store enhancer 的签名是 createStore => createStore,但是最简单的使用方法就是直接作为最后一个 enhancer 参数传递给 createStore() 函数。
示例: 自定义 Logger Middleware

import { createStore, applyMiddleware } from 'redux'
import todos from './reducers'

function logger({ getState }) {
  return next => action => {
    console.log('will dispatch', action)

    // 调用 middleware 链中下一个 middleware 的 dispatch。
    let returnValue = next(action)

    console.log('state after dispatch', getState())

    // 一般会是 action 本身,除非
    // 后面的 middleware 修改了它。
    return returnValue
  }
}

let store = createStore(
  todos,
  ['Use Redux'],
  applyMiddleware(logger)
)

store.dispatch({
  type: 'ADD_TODO',
  text: 'Understand the middleware'
})
// (将打印如下信息:)
// will dispatch: { type: 'ADD_TODO', text: 'Understand the middleware' }
// state after dispatch: [ 'Use Redux', 'Understand the middleware' ]

整个流程梳理

简单版本

看以下代码就知道大体流程了。
说白了,Store 增强器就是对生成的 store API 进行改造,这是它与中间件最大的区别(中间件不修改 store 的 API)
而改造 store 的 API 就要从它的缔造者 createStore 入手。例如,Redux 的 API applyMiddleware 就是一个 Store 增强器:

export default function applyMiddleware(...middlewares) {

//返回一个函数签名跟 createStore 一模一样的函数
//亦即返回的是一个增强版的 createStore
    return (createStore) => (reducer, preloadedState, enhancer) => {
      var store = createStore(reducer, preloadedState, enhancer)
      var dispatch = store.dispatch
      var chain = []

  // 提供给中间件的 API(其实都是 store 的 API)    
  var middlewareAPI = {
        getState: store.getState,
        dispatch: (action) => dispatch(action)
  }

   // 给中间件“装上” API  
      chain = middlewares.map(middleware => middleware(middlewareAPI))
      dispatch = compose(...chain)(store.dispatch)
// 例如,chain 为 [M3, M2, M1],而 compose 是从右到左进行“包裹”的
      // 那么,M1 的 dispatch 参数为 store.dispatch(见【降低逼格写法】的【锚点-2】)
      // 往后,M2 的 dispatch 参数为 M1 的中间件处理逻辑哥(见【降低逼格写法】的【锚点-3】)
      // 同样,M3 的 dispatch 参数为 M2 的中间件处理逻辑哥
      // 最后,我们得到串联后的中间件链:M3(M2(M1(store.dispatch)))
      //(这种形式的串联类似于洋葱,可参考文末的拓展阅读:中间件的洋葱模型)
      return {
        ...store,
        dispatch
      }
    }
  }
export function createStore (reducer,enhancer) {
// 如果传入了applymiddleweare,把createStore包装
    if(enhancer){
        return enhancer(createStore)(reducer)
    }

    let state = null
    const listeners = []
    const subscribe = (listener) => listeners.push(listener)
    const getState = () => state
    const dispatch = (action) => {
  //reducer只是返回对象,所以需要用每次 reducer(state, action) 的调用结果覆盖原来的 state
      state = reducer(state, action)
      listeners.forEach((listener) => listener())
    return action 
// 为了方便链式调用,dispatch 执行完毕后,返回 action
    }
    dispatch({}) // 初始化 state
    return { getState, dispatch, subscribe }
}

将 middleware 串连起来的必要性是显而易见的。

如果 applyMiddlewareByMonkeypatching 方法中没有在第一个 middleware 执行时立即替换掉 store.dispatch,那么 store.dispatch 将会一直指向原始的 dispatch 方法。也就是说,第二个 middleware 依旧会作用在原始的 dispatch 方法。

但是,还有另一种方式来实现这种链式调用的效果。可以让 middleware 以方法参数的形式接收一个 next() 方法,而不是通过 store 的实例去获取。

action:第三个参数备注一下
function readMsg(from){
    // getState,获取redux内已经登录的信息
    return (dispatch,getState)=>{
        axios.post('/user/readmsg',{from}).
            then(res=>{
                // 获取当前用户ID
                const userid = getState().user._id
                if(res.status == 200 && res.data.code == 0){
                    dispatch(msgRead({userid,from,num:res.data.num}))
                }
            })
    }
}
===================
const arrayThunk = ({dispatch,getState})=>next=>action=>{
    // 如果是函数,执行以下,参数是dispatch和getState
    //一下是多个中间件的情况
    if(Array.isArray(action)){
        action.forEach(v=>next(v))
    }
    if(typeof action == 'function'){
        return action(dispatch,getState)
    }

    // 默认,什么都没干
    return next(action)
}
=====
/* 降低逼格写法 */
function printStateMiddleware(middlewareAPI) { // 记为【锚点-1】,中间件内可用的 API
  return function (dispatch) {                 // 记为【锚点-2】,传入上级中间件处理逻辑(若无则为原 store.dispatch)

    // 下面记为【锚点-3】,整个函数将会被传到下级中间件(如果有的话)作为它的 dispatch 参数
    return function (action) { // 这货就叫做【中间件处理逻辑哥】吧
      console.log('state before dispatch', middlewareAPI.getState())
  
      var returnValue = dispatch(action) // 还记得吗,dispatch 的返回值其实还是 action
  
      console.log('state after dispatch', middlewareAPI.getState())

      return returnValue // 将 action 返回给上一个中间件(实际上可以返回任意值,或不返回)
    }
  }
}


Redux 有五个 API,分别是:

  • createStore(reducer, [initialState])
  • combineReducers(reducers)
  • applyMiddleware(...middlewares)
  • bindActionCreators(actionCreators, dispatch)
  • compose(...functions)

createStore 生成的 store 有四个 API,分别是:

  • getState()
  • dispatch(action)
  • subscribe(listener)
  • replaceReducer(nextReducer)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容