Redux Middleware 源码解析

发布于 2022-11-06 16:59:23 字数 7799 浏览 129 评论 0

作者从 2016 年开始接触 React+Redux,通过阅读 Redux 源码,了解了其实现原理。Redux 代码量不多,结构也很清晰,函数式编程思想贯穿着整个 Redux 源码,如纯函数,高阶函数,Curry,Compose。

本文首先会介绍函数式编程的思想,再逐步介绍 Redux 中间件的实现。看完本文,希望可以帮助你了解中间件实现的原理。

基本概念

Redux 是可预测的状态管理框架。它很好的解决多交互,多数据源的诉求。

Redux 设计理念有三个原则: 1. 单一数据源 2. State 只读 3. 使用纯函数变更 state 值。
基本概念原则解释 Store (1) 单一数据源 (2) State 只读 Store 可以看做是数据存储的一个容器。在这个容器里面,只会维护唯一的一个 State Tree。

Store 会给定4种基础操作方法:dispatch(action)、getState()、replaceReducer(nextReducer)、subscribe(listener)

根据单一数据源原则,所有数据会通过 store.getState() 方法调用获取。

根据 State 只读原则,数据变更会通过 store,dispatch(action) 方法。
Action(3) 使用纯函数变更 state 值 Action 可以理解为变更数据的信息载体。type 是变更数据的唯一标志,payload 是用来携带需要变更的数据。
格式为:const action = { type: 'xxx', payload: 'yyy' };
Reducer(3) 使用纯函数变更 state 值 Reducer 是个纯函数。负责根据获取 action.type 的内容,计算 state 数值。
reducer: prevState => action => newState。

正常的一个同步数据流为:view 层触发 actionCreator,actionCreator 通过 store.dispatch(action) 方法,变更 reducer。

但是面对多种多样的业务场景,同步数据流方式显然无法满足。对于改变 reducer 的异步数据操作,就需要用到中间件的概念。

函数式编程

函数式编程贯穿着 Redux 的核心。这里会简单介绍几个基本概念。如果你已经了解了函数式编程的核心技术,例如 高阶函数,compose, currying,递归,可以直接绕过这里。

我简单理解的函数式编程思想是: 通过函数的拆解,抽象,组合的方式去编程。复杂问题可以拆解成小粒度函数,最终利用组合函数的调用达成目的。

高阶函数

Higher order functions can take functions as parameters and return functions as return values.

接受函数作为参数传入,并能返回封装后函数。

Compose

Composes functions from right to left.

组合函数,将函数串联起来执行。就像 domino 一样,推倒第一个函数,其他函数也跟着执行。

首先我们看一个简单的例子。

// 实现公式: f(x) = (x + 100) * 2 - 100
const add = a => a + 100;
const multiple = m => m * 2;
const subtract = s => s - 100;
 
// 深度嵌套函数模式 deeply nested function,将所有函数串联执行起来。
subtract(multiple(add(200)));

上述例子执行结果为:500

compose 其实是通过 reduce() 方法,实现将所有函数的串联。不直接使用深度嵌套函数模式,增强了代码可读性。不要把它想的很难。

function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}


compose(subtract, multiple, add)(200);

Currying

Currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument

翻译过来是:把接受多个参数 的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

直接撸代码解释

// 实现公式: f(x, y, z) = (x + 100) * y - z;
const fn = (x, y, z) => (x + 100) * y - z;
fn(200, 2, 100);
  
// Curring实现 使用一层层包裹的单参匿名函数,来实现多参数函数的方法
const fn = x => y => z => (x + 100) * y - z;
fn(200)(2)(100);

Currying 只允许接受单参数。

Redux applyMiddleware.js

Redux 中 reducer 更关注的是数据逻辑转化,所以 Redux 中间件是为了增强 dispatch 方法出现的。如我们上面图,所描述的流程。中间件调用链,会在 dispatch(action) 方法之前调用。

所以 Redux 中间件实现核心目标是:改造 dispatch 方法。

redux 对中间件的实现,代码是很精简。整体都不超过20行。

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    const store = createStore(reducer, preloadedState, enhancer)
    let dispatch = store.dispatch
    let chain = []
 
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)
 
    return {
      ...store,
      dispatch
    }
  }
}

接下来,一步步的解析 Redux 在中间件实现的过程。

applyMiddleware.js 方法有三个主要的步骤,如下:

  1. 将所有的中间件组合在一起, 并保证最后一个执行的是 dispatch(action) 方法。
  2. 像 Koa 所有中间件对 ctx 的调用一样。保证所有的中间件都能访问到 Store。
  3. 最后将含有中间件调用链的新 dispatch 方法,合并到 Store 中。
  4. redux 对中间件的定义格式为:mid1 = store => next => action => { next(action) };

看到这里,你可能有这么几个疑问?

  1. 如何将所有的 middleware 串联执行在一起?并可以保证最后一个执行的是 dispatch 方法?
  2. 如何让所有的中间件都可以访问到 Store?
  3. 因为新形成的 dispatch 方法,为含有中间件调用链的方法结合。中间件如果调用 dispatch,岂不是会死循环在调用链中?
  4. 为什么将中间件格式定义为 mid1 = store => next => action => { next(action) } ?

为了解决这4个疑问,下面将针对相应问题,逐步解析。

中间件串联

疑问:如何将所有的 middleware 串联执行在一起?并可以保证最后一个执行的是 dispatch(action) 方法?

解决思路:

  1. 深度嵌套函数 / compose 组合函数方法,将所有的中间件串联起来。
  2. 封装最后一个函数作为 dispatch(action) 方法。
const middleware1 = action => action;
const middleware2 = action => action;
const final = action => store.dispatch(action);
/*
compose(...)将所有中间件串联
定义final作为最后执行dispatch的函数
*/
compose(final, middleware2, middleware1)(action)

中间件可访问 Store

疑问:如何让所有的中间件都可以访问到 Store?

可以参考我们对 Koa2 中间件的定义

const koaMiddleware = async(ctx, next) => {};

解决思路:

给每一个 middleware 传递 Store,保证每一个中间件访问到的都是一致的。

const middleware1 = (store, action) => action;
const middleware2 = (store, action) => action;
const final = (store, action) => store.dispatch(action);

如果我们想使用 compose 方法,将所有中间件串联起来,那就必须传递单一参数。根据上面函数式编程讲到的 currying 方法,对每个中间件柯里化处理。

// 柯里化处理参数
const middleware1 = store => action => action;
const middleware2 = store => action => action;
const final = store => action => store.dispatch(action);
 
// 将store保存在各个函数中 -> 循环执行处理。
const chain = [final, middleware2, middleware1].map(midItem => midItem(store));
compose(...chain)(action);

通过循环处理,将 store 内容,传递给所有中间件。这里就体现了 currying 的作用,延迟计算和参数复用。

中间件调用新 dispatch 方法死循环

疑问:因为新形成的 dispatch 方法,为含有中间件调用链的方法结合。中间件如果调用 dispatch,岂不是会死循环在调用链中?

new_dispatch = compose(...chain)(store.dispatch);
new_store = { ...store, dispatch: new_dispatch };

根据源码的解析,新和成 new_dispatch 是带有中间件调用链的新函数,并不是原来使用的 store.dispatch 方法。

如果根据上面的例子使用的方式传入 store:

 const chain = [final, middleware2, middleware1].map(midItem => midItem(store));

此时保存在各个中间件中的 store.dispatch 为已组合中间件 dispatch 方法,中间件如果调用 dispatch 方法,会发生死循环在调用链中。

解决思路:给定所有中间件的 dispatch 方法为原生 store.dispatch 方法,不是新和成的 dispatch 方法。

// 这就是为什么在给所有 middleware,共享 Store 的时候,会重新定义一遍 getState 和 dispatch 方法。
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

保证中间件不断裂

疑问:为什么将中间件格式定义为 mid1 = store => next => action => { next(action) } ?

上述例子有提到每次都会返回 action 给下一个中间件,例如 const middleware1 = store => action => action,如何保证中间件不会因为没有传递 action 而断裂?

这里必须说明的是:Koa 中间件可以通过调用 await next() 方法,继续执行下一个中间件,也可以中断当前执行,比如 ctx.response.body = ‘xxxx’(直接中断下面中间件的执行)。一般情况下,Redux 不允许调用链中断,因为我们最终需要改变 state 内容。(比如 redux-thunk 使用有意截断的除外)。

解决思路:如果可以保证,上一个中间件都有下一个中间件的注册,类似Koa对下一个中间件调用方式next(),不就可以保证了中间件不会断裂。

// 柯里化处理参数
const middleware1 = store => next => action => { log(1); next(action)};
const middleware2 = store => next => action => { log(2); next(action)};
// 中间件串联
const chain = [middleware1, middleware2 ].map(midItem => midItem({
dispatch: (action) => store.dispatch(action)}));
// compose(...chain) 会形成一个调用链,next 指代下一个函数的注册,如果执行到了最后 next 就是原生的 store.dispatch 方法
dispatch = compose(...chain)(store.dispatch);

总结

Redux applyMiddleware.js 机制的核心在于,函数式编程的 compose 组合函数,需将所有的中间件串联起来。为了配合 compose 对单参函数的使用,对每个中间件采用 currying 的设计。同时利用闭包原理做到每个中间件共享 Store。

另外,Redux / React 应用函数式编程思想设计,其实是通过组合和抽象来减低软件管理复杂度。

简单写了个学习例子,参考:https://github.com/Linjiayu6/learn-redux-code

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

遥远的她

暂无简介

文章
评论
26 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文