探究 redux 源码-衍生-中间件 思想
本文主要是阅读 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 流程
applyMiddleware 封装之后
可以看出 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"}
用图形象点就是
这样 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 处理。
用法
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 技术交流群。
上一篇: 我的 Vim 配置分享
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
@dioxide
这就是三阶函数。
这就是二阶。
看他要执行几次才能到最内部的函数代码,就是几阶。统称就是高阶函数...
三阶函数,二阶函数是怎样的概念?