探究 redux 源码-衍生-中间件 思想

发布于 2022-11-29 17:24:36 字数 10408 浏览 111 评论 2

本文主要是阅读 redux 实现方式的时候,思路的一些拓展。大概是在三四个月前就看过redux源码,一直想写一些东西。但是迫于项目的紧急性,以及个人能力精力有限,就搁浅了。现在又重新看,而且很多时候,看懂一些东西可能不难,但是真正深入进去研究,会发现很多东西并不是很清楚,这就需要多思考一些,再写下来能有清晰的思路就更难了。这次的文章需要你对 redux,react-redux 都有一定的了解,很多地方我没有做过多的解释,还有本文不完美的地方,还请指出。

redux 基础

我们先大概过一下 redux 暴露的几个方法。

// index.js
export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
}

createStore
一个工厂函数传入reducer,创建store,返回几个函数,主要是dispatch,getState,subscribe,replaceReducer,以及结合rx这种发布订阅库的symbol($$observable)

combineReducers
把单个的reducer组合成一个大的reducer。其实就是返回一个新的reducer函数,去遍历执行所有的子reducer,并返回state。

bindActionCreators
把我们写的一个js中的好多ActionCreator 通过遍历搞的一个对象里,并返回。

applyMiddleware
一个三阶函数,是用来改写store的dispatch方法,并把所有的中间件都compose串联起来,通过改写dispatch,来实现redux功能的拓展。

compose
一个组合多个middleware的方法,通过reduceRight方法(同理也可以是reduce),把传进来的middleware串成一条链,也可以看成回调接回调,一个预处理的方法。

redux-middleware

接触过后端的同学,对中间件这个概念一定不陌生。像 node 中的 express,koa 框架,middleware 都起到了重要作用。redux 中的实现方式不太一样,不过原理思想都是差不多的,都是链式组合,可以应用多个中间件。它提供了 action 发起之后,到达 reducer 之前的拓展功能。可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。

我们从 redux 中 applyMiddleware 使用入口开始研究。

中间件

  //日志中间件1
  const logger1 = store => next => action => {
    console.log('logger1 start', action);
    next(action);
    console.log('logger1 end', action);
  }
  
  //日志中间件2
  const logger2 = store => next => action => {
    console.log('logger2 start', action);
    next(action);
    console.log('logger2 end', action);
  }

为什么中间件要定义成这种三阶的样子呢,当然是中间件的消费者(applyMiddleware)规定的。

先通过一个小栗子看一下 middleware 的使用。

  //定义一个reducer
  const todoList = [];
  function addTodo(state = todoList, action) {
    switch (action.type) {
      case 'ADD_TODO':
        return [...state, action.text];
        break;
      default:
        return state;
    }
  }

 //创建store
 //为了先减轻其他方法带来的阅读困难,我选用直接使用applyMiddleware的方法创建store
  
  import { createStore, applyMiddleware } from 'redux';
  
  const store = applyMiddleware(logger1, logger2)(createStore)(reducer);
  

 // store注入Provider    
  ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));

通过 applyMiddleware 执行可以得到一个 store,store 再通过 react-redux 中的 provider 注入。此时得到的 store 就是被改造了 dispatch 的。通过图来形象的解释一下:

默认的 redux 流程

default

applyMiddleware 封装之后

middleware1

可以看出 redux 在事件或者某个函数调用后,执行 action(可能是 bindActionCreators 处理后的),由于 bindActionCreator 会去调用 dispatch,

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}

dispatch 内部会把 currenReducer 执行,并把监听者执行。实现 view 更新。但是经过 applyMiddleware 的包装,store 里面的被封装,在调动 action 之后,执行封装后的 dispatch 就会经过一系列的中间件处理,再去触发 reducer。

然后我们再通过研究源码,看他是怎么实现的封装 dispatch。

思路可以从通过 applyMiddleware 创建 store 一点一点的看。

//applyMiddleware 源码

middlewares => createStore => (reducer, preloadedState) => {

 // 第一步先创建一个store
  var store = createStore(reducer, preloadedState, enhancer)
  
 // 缓存dispatch,原store的dispatch要改写。
  var dispatch = store.dispatch
  
 // 定义chain来存放 执行后的二阶中间件
  var chain = []

 // middleware 柯理化的第一个参数。参照logger1的store,这里只保留getState,和改造后的dispatch两个方法。
  var middlewareAPI = {
    getState: store.getState,
    dispatch: (action) => dispatch(action)
  }
  
  // 把中间件处理一层,把getState,dispatch方法传进去,也就是中间件柯理化第一次的store参数。
  // 这样能保证每个中间件的store都是同一个,柯理化的用途就是预置参数嘛。
  chain = middlewares.map(middleware => middleware(middlewareAPI))
  
  // 串联起所有的中间件,dispatch重新赋值,这样调用dispatch的时候,就会穿过所有的中间件。
  dispatch = compose(...chain)(store.dispatch)

  return {
    ...store,
    dispatch
  }
}

compose 还是比较重要的

//compose
其实compose是函数式编程中比较重要的一个方法。上面调用compose的时候可见是一个二阶函数。
 
const compose = (...funcs) => {
  
  //没有参数,那就返回一个function
  if (!funcs.length) {
    return arg => arg
  }
  //一个中间件,返回它
  if (funcs.length === 1) {
    return funcs[0];
  }
  // 最后一个
  var last = funcs[funcs.length -1];
  
  // 复制一份,除去last
  var rest = funcs.slice(0, -1);
  
  // 返回函数,可以接收store.dispatch。
  // reduceRight 反向迭代。
  
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}

compose 执行

  • chain 中都是已经预置 middlewareAPI 参数后的二阶函数。执行传入的参数都是 形参 next。
  • 通过执行 compose(...chain)(store.dispatch),last 是最后一个中间件,执行并传入 store.dispatch,返回一个只剩一阶的 (action) => {}, 不过已经预置了 next 参数,也就是 store.dispatch
  • 然后 last(...args) 返回的结果传入 reduceRight 的回调,对应形参是 composed。
  • f 是 rest 的最后一项, 执行并把 composed 传入,等同于 f 形参中的 next... 得到的结果也是一阶函数,预置的 next 是 last(...args) ...
  • 以此类推。这样,就形成了一个嵌套多层的语句。
    类似于 logger1(logger2(store.dispatch),当然这只是一个比喻。
    只不过到第一个 middleware 的时候,是二阶函数传入 next 执行,得到一阶函数返回赋值给 dispatch,这时的一阶函数已经变成了形似这样:

    function (action) {
      console.log('logger1 start', action);
      next(action);
      console.log('logger1 end', action);
    }
    

经过 compose 之后的 dispatch 执行

  • 返回的 store 中 dispatch 被修改,执行 store.dispatch 的时候,也就是这个函数执行。
  • 当执行到 next(action) 的时候,会调用已经预置的 next 函数,也就是第二个中间件的 (action) => {},依次类推。直到最后一个中间件,他的 next 函数是 store.dispatch 函数,执行并把 action传入。
  • 执行完最后一个中间件的 next(action),也就是初始的 dispatch。next 后面的代码再执行,再逆向把中间件走一遍,直到第一个中间件执行完毕。
    就会出现这种效果

    start logger1 Object {type: "ADD_TODO", text: "defaultText"}
    start logger2 Object {type: "ADD_TODO", text: "defaultText"}
    dispatch()
    end logger2 Object {type: "ADD_TODO", text: "defaultText"}
    end logger1 Object {type: "ADD_TODO", text: "defaultText"}
    

用图形象点就是

middleware

这样 redux middleware 的执行流程就搞清楚了。

应用 applyMiddleware 的方式

import { createStore, applyMiddleware } from 'redux';
1. compose(applyMiddleware(logger1, logger2))(createStore)(reducer);
2. applyMiddleware(logger1, logger2)createStore)(reducer);
3. createStore(reducer, [], applyMiddleware(logger1, logger2));

createStore源码中有一个判断,

createStore(reducer, preloadedState, enhancer) => {
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    // 所以第三种直接传入applyMiddleware(logger1, logger2),效果是一样的。
    return enhancer(createStore)(reducer, preloadedState)
  }
}

第一种先 compose 同理。一个参数的时候会返回 applyMiddleware,变形之后也是一样的。

enhancer 的用法很多种,不仅仅是 applyMiddleware,比如 Redux-Devtools,都是利用了 compose 函数。自定义开发一些拓展功能还是很强大的,redux 里的 compose 是处理三阶函数的,恰巧createStore,applyMiddleware 都是三阶函数,都可以通过 compose 串联起来。不禁感叹函数式编程思维的强大啊。

应用异步 action

redux-thunk

简单来说,就是 dispatch(action),action 可以是 function,当然这种写法需要配合 bindActionCreator 处理。

actionCreator 之前都是返回一个 {type: 'UPDATE', text: 'aaa'} 这样的简单对象。通过 thunk 中间件,可以处理返回 function 的情况。

const reduxThunk = store => next => action => {
  if (typeof action === 'function') {
    console.log('thunk');
    return action(store.dispatch);
  }
  return next(action);
}

//action 可能是这样。
const addAsync = function() {
  return (dispatch) => {
    setTimeout(() => {
      dispatch({ type: 'ADD_TODO', text: 'AsyncText' })
    }, 1000)
  }
}

redux-promise

用来处理 actions 返回的是 promise 对象的情况。其实道理很简单,thunk 去判断传进中间件的 action 是不是 function,这里就判断是不是 promise 就行了。

//判断promise
function isPromise(val) {
  return val && typeof val.then === 'function';
}

const reduxPromise = store => next => action => {
  return isPromise(action)
    ? action.then(store.dispatch)
    : next(action);
}

// 源码还多了一个判断,判断action是不是标准的flux action对象(简单对象,包含type属性...)

express 中的 middleware

当一个客户端的 http 请求过来的时候,匹配路由处理前后,会经过中间件的处理,比如一些 CORS 处理,session 处理。

express-mid

用法

var app = express();

app.use(function (req, res, next) {
  console.log('Time:', Date.now());
  next();
});

app.use(middleware1);
app.use(middleware2);
app.use(middleware3);

app.listen(3000);

每次访问这个app应用的时候,都会执行

模拟

看了源码,自己模拟一下,当然是很简单的用法了。这是应用层的中间件,要实现路由器层的话,只需要根据路由 保存不同的数组就好了,然后匹配。

const http = require('http');
function express () {
  const app = function(req, res) {
    let index = 0;
    //重点在于next函数的实现,express是用一个数组维护的。
    function next() {
      const routes = app.route;
      routes[index++](req, res, next);
    }
    next();
  };
  
  app.route = [];
  
  // 很明显use 是往数组里push。
  app.use = function (callback) {
    this.route.push(callback);
  };
  
  // listen函数是一个语法糖,利用http模块
  app.listen = function(...args) {
    http.createServer(app).listen(...args);
  }

  return app;
}

const app = express();

app.use((req, res, next) => {
  setTimeout(() => {
    console.log('async');
    next();
  }, 1000);
});

app.use((req, res, next) => {
  console.log( 'logger request url:', req.url);
  next();
});


app.listen(3333);

假总结

现在 Web 的中间件概念,都区别于最早严格意义上的中间件,其实我们现在的很多编程思想都是借鉴的先驱提出的一些东西。JAVA 中类似的是 AOP,即面向切面编程,以补充 OOP(面向对象)多个对象公用某些方法时造成的耦合。

目前js中见到的中间件思想用法都是差不多的,只有调用 next,程序才会继续往下执行,没有 next,可以抛出异常等。只不过 redux 使用的函数式编程思想,用法偏函数式一些。

demo 代码我会放到 middleware-demo 目录里,可以 clone 下来操作一番。链接

先到这,下次衍生就是函数式编程了。

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

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

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

只为一人 2022-05-04 10:36:21

@dioxide

const func = a => b => c => {
   //code
}

这就是三阶函数。

const func = a => b => {
   //code
}

这就是二阶。
看他要执行几次才能到最内部的函数代码,就是几阶。统称就是高阶函数...

路弥 2022-05-02 15:03:53

三阶函数,二阶函数是怎样的概念?

~没有更多了~

关于作者

明月夜

暂无简介

0 文章
0 评论
24 人气
更多

推荐作者

已经忘了多久

文章 0 评论 0

15867725375

文章 0 评论 0

LonelySnow

文章 0 评论 0

走过海棠暮

文章 0 评论 0

轻许诺言

文章 0 评论 0

信馬由缰

文章 0 评论 0

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