摘要:最近在看的源码,发现在使用中间件的源码中,有一个对闭包非常巧妙的使用,解决了鸡生蛋,蛋生鸡的问题,特分享给大家。中间件的函数签名形式如下函数体中的函数用于根据中间件生成经过的中间件链。
最近在看Redux的源码,发现Redux在使用中间件applyMiddleware.js的源码中,有一个对闭包非常巧妙的使用,解决了“鸡生蛋,蛋生鸡”的问题,特分享给大家。
Redux中间件的函数签名形式如下:
({dispatch, getState}) => next => action => { // 函数体 }
applyMiddleware.js中的函数applyMiddleware(...middlewares)用于根据中间件生成action经过的中间件链。先来看一个错误版本的实现:
/* * @param {...Function} middlewares The middleware chain to be applied. * @returns {Function} A store enhancer applying the middleware. */ export default function applyMiddleware(...middlewares) { return (createStore) => (reducer, initialState, enhancer) => { var store = createStore(reducer, initialState, enhancer) var chain = [] var middlewareAPI = { getState: store.getState, dispatch: store.dispatch } chain = middlewares.map(middleware => middleware(middlewareAPI)) var dispatch = compose(...chain)(store.dispatch) //compose(f, g, h) 等价于函数 //(...args)=>f(g(h(args))) return { ...store, dispatch } }
核心逻辑是chain = middlewares.map(middleware => middleware(middlewareAPI))和dispatch = compose(...chain)(store.dispatch)这两行。第1句代码是根据中间件生成一个数组chain,chain的元素是签名为next => action => {...}形式的函数,每个元素就是最终中间件链上的一环。第2句代码利用compose函数,将chain中的函数元素组成一个“洋葱式”的大函数,chain的每个函数元素相当于一层洋葱表皮。Redux发送的每一个action都会由外到内依次经过每一层函数的处理。假设有3层函数,从外到内依次是a,b,c,函数的实际调用过程是,a接收到action,在a函数体内会调用b(a的参数next,指向的就是b),并把action传递给b,然后b调用c(b的参数next指向的就是c),同时也把action传递给c,c的参数next指向的是原始的store.dispatch,因此是action dispatch的最后一环。这样分析下来,程序是没有问题的,但当我们的中间件需要直接使用dispatch函数时,问题就出来了。例如,常用于发送异步action的中间件redux-thunk,就需要在异步action中使用dispatch:
export function fetchPosts(subreddit) { return function (dispatch) { dispatch(requestPosts(subreddit)) return fetch(`https://www.reddit.com/r/${subreddit}.json`) .then( response => response.json(), error => console.log("An error occured.", error) ) .then(json => dispatch(receivePosts(subreddit, json)) ) } }
fetchPosts使用的dispatch,是redux-thunk传递过来的,指向的是middlewareAPI对象中的dispatch,实际等于store.dispatch。当执行dispatch(requestPosts(subreddit))时,这个action直接就到了最后一环节的处理,跳过了redux-thunk中间件之后的其他中间件的处理,显然是不合适的。我们希望的方式是,这个action依然会从最外层的中间件开始,由外到内经过每一层中间件的处理。所以,这里使用的dispatch函数不能等于store.dispatch,应该等于compose(...chain)(store.dispatch),只有这样,发送的action才能经过每一层中间件的处理。现在问题出来了,chain = middlewares.map(middleware => middleware(middlewareAPI))需要使用dispatch = compose(...chain)(store.dispatch)返回的dispatch函数,而dispatch = compose(...chain)(store.dispatch)的执行又依赖于chain = middlewares.map(middleware => middleware(middlewareAPI))的执行结果,我们进入死循环了。
问题的解决方案就是闭包。当我们定义middlewareAPI的dispatch时,不直接把它指向store.dispatch,而是定义一个新的函数,在函数中引用外部的一个局部变量dispatch,这样就形成了一个闭包,外部dispatch变量的变化会同步反映到内部函数中。如下所示:
export default function applyMiddleware(...middlewares) { return (createStore) => (reducer, initialState, enhancer) => { var store = createStore(reducer, initialState, enhancer) var dispatch = store.dispatch; // 需要有初始值,保证中间件在初始化过程中也可以正常使用dispatch var chain = [] var middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) // 通过闭包引用外部的dispatch变量 } chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) //compose(f, g, h) 等价于函数 //(...args)=>f(g(h(args))) return { ...store, dispatch } }
这样,“鸡生蛋,蛋生鸡”的问题就解决了。如果这个例子对你来说太复杂,可以用下面这个简化的例子帮助你理解:
const middleware = ({dispatch}) => (next) => (number) => { console.log("in middleware"); if(number !== 0){ return dispatch(--number); } return next(number); } function test() { var dispatch = (number) => { console.log("original dispatch"); return number; }; var middlewareAPI = { dispatch } dispatch = middleware(middlewareAPI)(dispatch); return { dispatch } } var {dispatch} = test(); dispatch(3); //输出: "in middleware" "original dispatch" const middleware = ({dispatch}) => (next) => (number) => { console.log("in middleware"); if(number !== 0){ return dispatch(--number); } return next(number); } function test() { var dispatch = (number) => { console.log("original dispatch"); return number; }; var middlewareAPI = { dispatch: (number) => {dispatch(number);} } dispatch = middleware(middlewareAPI)(dispatch); return { dispatch } } var {dispatch} = test(); dispatch(3); //输出 "in middleware" "in middleware" "in middleware" "in middleware" "original dispatch"
第二种方式,middleware中dispatch的number会再次经历中间件的处理,当number=3,2,1,0时,都会进入一次middleware函数,当number=0时,next(0)调用的是test中定义的初始dispatch函数,因此不再经过middleware的处理。
欢迎关注我的公众号:老干部的大前端,领取21本大前端精选书籍!
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/88551.html
摘要:前端日报精选第一问连接的问题该怎么答路由轻量级函数式编程第章闭包对象开始使用和近阶段学习总结中文基础图表之一掘金中的模式匹配众成翻译字符串的扩展设计模式系列二之建造者模式附案例源码掘金通告教你写完美简历应聘名企外企知 2017-09-12 前端日报 精选 第一问:TCP连接的问题该怎么答Vue-Router(vue路由)JavaScript轻量级函数式编程-第7章: 闭包vs对象开始使...
摘要:源码个人觉得后的代码就是,后返回的任然是一个函数,可以接受参数,在后面介绍的代码中有所涉及。源码中的获取对象供后面使用,里的对应着对象里的,方法可以获取,也就是对象树的总数据。 redux介绍 redux给我们暴露了这几个方法 { createStore, combineReducers, bindActionCreators, applyMiddleware, c...
摘要:源码个人觉得后的代码就是,后返回的任然是一个函数,可以接受参数,在后面介绍的代码中有所涉及。源码中的获取对象供后面使用,里的对应着对象里的,方法可以获取,也就是对象树的总数据。 redux介绍 redux给我们暴露了这几个方法 { createStore, combineReducers, bindActionCreators, applyMiddleware, c...
摘要:面试题来源于网络,看一下高级前端的面试题,可以知道自己和高级前端的差距。 面试题来源于网络,看一下高级前端的面试题,可以知道自己和高级前端的差距。有些面试题会重复。 使用过的koa2中间件 koa-body原理 介绍自己写过的中间件 有没有涉及到Cluster 介绍pm2 master挂了的话pm2怎么处理 如何和MySQL进行通信 React声明周期及自己的理解 如何...
阅读 1276·2021-09-23 11:51
阅读 1395·2021-09-04 16:45
阅读 635·2019-08-30 15:54
阅读 2088·2019-08-30 15:52
阅读 1607·2019-08-30 11:17
阅读 3110·2019-08-29 13:59
阅读 2025·2019-08-28 18:09
阅读 391·2019-08-26 12:15