摘要:最近在看的源碼,發現在使用中間件的源碼中,有一個對閉包非常巧妙的使用,解決了雞生蛋,蛋生雞的問題,特分享給大家。中間件的函數簽名形式如下函數體中的函數用于根據中間件生成經過的中間件鏈。
最近在看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本大前端精選書籍!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/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聲明周期及自己的理解 如何...
閱讀 1264·2021-09-23 11:51
閱讀 1370·2021-09-04 16:45
閱讀 626·2019-08-30 15:54
閱讀 2076·2019-08-30 15:52
閱讀 1594·2019-08-30 11:17
閱讀 3098·2019-08-29 13:59
閱讀 2010·2019-08-28 18:09
閱讀 381·2019-08-26 12:15