Redux-中间件原理总结

演化流程

参考:官网

我们都知道使用 createStore 创建的 “纯正” store 只支持普通对象类型的 action,而且会立即传到 reducer 来执行。
但是,如果你用 applyMiddleware(中间件) 来套住 createStore 时,middleware 可以修改 action 的执行并支持执行 dispatch intent(意图)Intent 一般是异步操作如 Promise、Observable 或者 Thunk。

像 redux-thunk 或 redux-promise 这样支持异步的 middleware 都包装了 store 的 dispatch() 方法,以此来让你 dispatch一些除了 action 以外的其他内容,例如:函数或者 Promise你所使用的任何 middleware 都可以以自己的方式解析你 dispatch 的任何内容并继续传递 actions 给下一个 middleware。比如,支持 Promise 的 middleware 能够拦截 Promise,然后为每个 Promise 异步地 dispatch 一对 begin/end actions。

当 middleware 链中的最后一个 middleware 开始 dispatch action 时,这个 action 必须是一个普通对象。这是同步式的 Redux 数据流开始的地方

这也是我们异步Action的主要方式。

工具函数Action Creator

function addTodo(text) {
  return {
    type: ADD_TODO,
    text,
  }
}

需求

  1. 在action发起和每次state被计算完成时都将它们记录下来,添加日志功能
  2. 在action中添加崩溃报告(生产过程中报告 JavaScript 的错误)

尝试1
手动记录

	// # 1. 手动记录
	let action = addTodo('use Redux')
	console.log('dispatching 分配开始', action)
	store.dispatch(action)
	console.log('开始记录改变值 ', store.getState())

尝试2
封装为函数,每次都要将其导入

	// # 2. 封装为函数

	function dispatchAndLog(store, action) {
	  console.log('dispatching 分配开始', action)
	  store.dispatch(action)
	  console.log('开始记录改变值 ', store.getState())
	}
	// 然后用它替换 store.dispatch()
	dispatchAndLog(store, addTodo('Use Redux'))

尝试3(Monkeypatching ):
替换dispatch,用新包装的dispatch替换store的dispatch

Monkeypatching :在运行时动态修改模块、类或函数,通常是添加功能或修正缺陷。这里就是在有中间件加入的时候,动态替换store的dispatch方法。

	// # 3. 替换dispatch
	// 获取之前的dipatch
	let next = store.dispatch
	// 替换dispatch
	store.dispatch = function dispatchAndLog(store, action) {
	  console.log('dispatching 分配开始', action)
	  // dipatch() 返回值为:要dispatch的action; 及参数action。方便继续分发,后续中间件可以继续使用
	  let resulte = next(action)
	  console.log('开始记录改变值 ', store.getState())
	  return resulte
	}

尝试4
如果有多个中间件;如上面提及的需求2,且各中间件为独立模块,就需要封装尝试3的替换方式

	// # 4. 不同功能封装替换store.dispatch方法
	
	//记录日志 封装替换dispatch
	function patchStoreToAddLogging(store) {
	  let next = store.dispatch
	  // 替换dispatch方法
	  store.dispatch = function dispatchAndLog(action) {
	    console.log('dispatching', action)
	    let result = next(action)
	    console.log('next state', store.getState())
	    return result
	  }
	}
	// 记录崩溃 封装替换dispatch
	function patchStoreToAddCrashReporting(store) {
	  let next = store.dispatch
	  // 替换dispatch方法
	  store.dispatch = function dispatchAndReportErrors(action) {
	    try {
	      return next(action)
	    } catch (err) {
	      console.error('捕获一个异常!', err)
	      Raven.captureException(err, {
	        extra: {
	          action,
	          state: store.getState(),
	        },
	      })
	      throw err
	    }
	  }
	}
	
	// 不同中间件依次替换store.dispatch方法,新的dispatch,就包含有2个中间的功能
	patchStoreToAddLogging(store)
	patchStoreToAddCrashReporting(store)

尝试5
将替换dispatch抽离出来,做为一个方法,隐藏替换dispatch(隐藏Monkeypatching )

// # 5. 利用applyMiddlewareByMonkeypatching 隐藏替换dispatch过程
function logger(store) {
  let next = store.dispatch // 步骤1:获取该中间件前面的 dispatch方法

  // 我们之前的做法: 直接替换dispatch
  // store.dispatch = function dispatchAndLog(action) {}

  // 直接返回新dipatch方法,然后后续中间件直接通过store.dispatch获取; 如步骤1
  return function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

function applyMiddlewareByMonkeypatching(store, middlewares) {
  // 为数组
  middlewares = middlewares.slice()
  // 反序数组 包装由里向外 运行由外向里
  middlewares.reverse()

  middlewares.forEach((middleware) => {
    // 依次包装dispatch 包装由里向外 运行由外向里
    store.dispatch = middleware(store)
  })
}

// 运用
applyMiddlewareByMonkeypatching(store, [logger, crashReporter])

尝试6
尝试避免:每次封装中间件时,都需要替换dispatch,然后封装下个中间件,又需要通过store.dispatch获取。
改进思路:通过每次封装中间件时,接受一个next函数(上个中间件返回的dispatch),返回一个dispatch,作为下个中间件的的参数。最后在同一替换dispatch。

好处:
相比尝试5最大的好处,在调用中间的next函数,是调用上一个中间件返回的dispatch。如果直接调用store.dispatch,那么这个操作会再次遍历包含当前 middleware 在内的整个 middleware 链。造成死循环。

这也是为什么在中间件调用千万别在某个中间件中直接调用store.dispatch, 不然会无限循环的原因。


// # 6. 移除替换dipatch功能 提取之前的dipatch作为参数传入下一个中间件
// 日志功能中间件
function logger(store) {
  // 传入next()的dipatch函数, 并返回一个dispatch函数,并作为下个中间件的next函数
  return function wrapDispatchToAddLogging(next) {
    return function dispatchAndLog(action) {
      console.log('dispatching', action)
      let result = next(action)
      console.log('next state', store.getState())
      return result
    }
  }
}

// 柯里化函数
// 日志功能
const logger = (store) => (next) => (action) => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}
// 捕获异常功能
const crashReporter = (store) => (next) => (action) => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState(),
      },
    })
    throw err
  }
}

// 包装中间件,并取得最终的dipatch方法 并返回一个 store 的副本
function applyMiddleware(store, middlewares) {
  // 为数组
  middlewares = middlewares.slice()
  // 反序数组 包装由里向外 运行由外向里
  middlewares.reverse()
  
  //相对于尝试5,代码的改进
  let next = store.dispatch
  middlewares.forEach((middleware) => {
    next = middleware(store)(next)
  })

  return Object.assign({}, store, { dispatch })
}

推到结束!

最终方法(redux源生API的方式):

不同于尝试6:为了保证你只能应用 middleware 一次,它作用在 createStore() 上而不是 store 本身。因此它的签名不是 (store, middlewares) => store, 而是 (…middlewares) => (createStore) => createStore

由于在使用之前需要先应用方法到 createStore() 之上有些麻烦,createStore() 也接受将希望被应用的函数作为最后一个可选参数传入。

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err
  }
}

// 然后是将它们引用到 Redux store 中
import { createStore, combineReducers, applyMiddleware } from 'redux'

let todoApp = combineReducers(reducers)
let store = createStore(
  todoApp,
  // applyMiddleware() 告诉 createStore() 如何处理中间件
  applyMiddleware(logger, crashReporter)
)

// 就是这样!现在任何被发送到 store 的 action 都会经过 logger 和 crashReporter:
// 将经过 logger 和 crashReporter 两个 middleware!
store.dispatch(addTodo('Use Redux'))

源码解析

参考redux之applyMiddleware源码解析

…后续补充

redux-thunk源码分析

参考:React 之 Redux 异步处理——redux-thunk

…后续补充

redux-saga源码分析

…后续补充

posted @ 2020-08-03 23:46  CD、小月  阅读(9)  评论(0编辑  收藏  举报  来源