摘要:好處就是不再需要能夠處理異步的中間件了。不過,它是一個研究中間件很好的范本。執行它,返回的是由第二層函數組成的中間件數組。也就是說呀同學們,除了最后一個中間件的是原始的之外,倒數往前的中間件傳入的都是上一個中間件的邏輯函數。
本文是『horseshoe·Redux專題』系列文章之一,后續會有更多專題推出
來我的 GitHub repo 閱讀完整的專題文章
來我的 個人博客 獲得無與倫比的閱讀體驗
Redux暴露非常少的API,優雅的將單向數據流落地。但有這些,Redux的作者Dan Abramov仍然覺得遠遠不夠。一個工具的強大之處體現在它的擴展能力上。Redux的中間件機制讓這種擴展能力同樣變的異常優雅。
中間件在前端的意思是插入某兩個流程之間的一段邏輯。具體到Redux,就是在dispatch一個動作前后插入第三方的處理函數。
使用還記得嗎?Store構造器createStore有三個參數,第三個參數叫做enhancer,翻譯過來就是增強器。我們先將enhancer按下不表,并且告訴你其實Redux的另一個APIapplyMiddleware就是一個enhancer。
import { createStore, combineReducers, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import logger from "redux-logger"; import { userReducer } from "./user/reducer"; import { todoReducer } from "./todo/reducer"; const reducers = combineReducers({ userStore: userReducer, todoStore: todoReducer, }); const enhancer = applyMiddleware(thunk, logger); const store = createStore(reducers, null, enhancer); export default store;
只需要把所有中間件依次傳入applyMiddleware,就生成了一個增強器,它們就可以發揮作用了。
如果preloadedState為空,enhancer可以作為第二個參數傳入。看源代碼:
if (typeof preloadedState === "function" && typeof enhancer === "undefined") { enhancer = preloadedState; preloadedState = undefined; } if (typeof enhancer !== "undefined") { if (typeof enhancer !== "function") { throw new Error("Expected the enhancer to be a function."); } return enhancer(createStore)(reducer, preloadedState); } if (typeof reducer !== "function") { throw new Error("Expected the reducer to be a function."); }服務器請求
一個組件免不了向服務器請求數據,然而開發者不希望組件內部有過多的邏輯,請求應該封裝成函數給組件調用,同時組件需要實時獲取請求的狀態以便展示不同的界面。最好的辦法就是將請求也納入Redux的管理中。
import api from "./api"; export const fetchMovieAction = () => { dispatch({ type: "FETCH_MOVIE_START" }); api.fetchMovie().then(res => { dispatch({ type: "FETCH_MOVIE_END", payload: { movies: res.data } }); }).catch(err => { dispatch({ type: "FETCH_MOVIE_ERROR", error: true, payload: { msg: err } }); }); };
import React, { Component } from "react"; import { connect } from "react-redux"; import { fetchMovieAction } from "./actions"; import Card from "./Card"; class App extends Component { render() { const { movies } = this.props; return ({movies.map(movie =>); } componentDidMount() { this.props.fetchMovie(); } } const mapState = (state) => { return { movies: state.payload.movies, }; }; const mapDispatch = (dispatch) => { return { fetchMovie: () => dispatch(fetchMovieAction()), }; }; export default connect(mapState, mapDispatch)(App);)}
大功告成了。
只需要將請求封裝成一個函數,然后偽裝成Action被發射出去,請求調用前后,真正的Action會被發射,在Store中存儲請求的狀態,并且能夠被組件訂閱到。
異步Action你是不是發現了什么?對咯,這里的Action不是一個純對象。
因為請求一定是一個函數,為了讓請求入會,只能反過頭來修改大會章程。但是大會章程豈能隨便推翻,這時意見領袖出來說話了:
當初規定Action必須是一個純對象不是為了搞個人崇拜,而是出于實際需要。因為reducer必須是一個純函數,這決定了dispatch的參數Action必須是一個帶type字段的純對象。現如今我們要拉異步請求入會,而中間件又可以中途攔截做一些處理,那Action為什么不能是一個函數呢?Action必須是一個純對象這種說法是完完全全的教條主義!
大家還動腦筋想出了一個異步Action的名頭,這下函數類型的Action終于名正言順了。
閉包你是不是還發現了什么?對咯,請求函數中的dispatch哪去了。
不知道,可能會報錯吧(無辜臉)。
其實我們還有一件事沒干:把dispatch方法偷渡到請求函數中。
export const fetchMovieAction = () => { return (dispatch) => { dispatch({ type: "FETCH_MOVIE_START" }); api.fetchMovie().then(res => { dispatch({ type: "FETCH_MOVIE_END", payload: { movies: res.data } }); }).catch(err => { dispatch({ type: "FETCH_MOVIE_ERROR", error: true, payload: { msg: err } }); }); }; };
很簡單哪,加一個閉包,dispatch從返回函數的參數中偷渡進來。
腦洞我們要的不就是一個dispatch方法么,我能不能這樣:
export const fetchMovie = (dispatch) => { dispatch({ type: "FETCH_MOVIE_START" }); api.fetchMovie().then(res => { dispatch({ type: "FETCH_MOVIE_END", payload: { movies: res.data } }); }).catch(err => { dispatch({ type: "FETCH_MOVIE_ERROR", error: true, payload: { msg: err } }); }); };
const mapDispatch = (dispatch) => { return { fetchMovie: () => fetchMovie(dispatch), }; };
貌似是能行得通的,只不過這時候請求函數已經不能叫Action了。考慮到之前請求函數偽裝成Action渾水摸魚,還要插入中間件來幫助特殊處理,我們這樣做也不過分是吧。
好處就是不再需要能夠處理異步Action的中間件了。
壞處就是這不符合規范,是我的腦洞,闖了禍不要打我(蔑視)。
redux-thunk前面多次提到處理異步Action的中間件,到底是何方神圣?
市面上流行的方案有很多種,我們挑最簡單的一種來說一說(都不點贊怪我咯)。
redux-thunk算是Redux官方出品的異步請求中間件,但是它沒有集成到Redux中,原因還是為了擴展性,社區可以提出各種方案,開發者各取所需。
讓我們來探討一下redux-thunk的思路:原來Action只有一種,就是純對象,現在Action有兩種,純對象和異步請求函數。只不過多了一種情況,不算棘手嘛。如果Action是一個對象,不為難它直接放走;如果Action是一個函數,就地執行,調用異步請求前后,真正的Action自然會釋放出來,又回到第一步,放它走。
這是redux-thunk簡化后的代碼,其實源代碼也跟這差不多。是不是很恐慌?
const thunk = ({ dispatch, getState }) => next => action => { if (typeof action === "function") { return action(dispatch, getState); } return next(action); };
上面的函數就部署在我的個人博客中用來處理異步請求,完全沒有問題。既然它這么簡單,而且可以預計它萬年不會變,那我為什么要憑空多一個依賴包。就將它放在我的眼皮底下不是挺好的嘛。
不過,它是一個研究中間件很好的范本。
我們先將thunk先生降級成普通函數的寫法:
const thunk = function({ dispatch, getState }) { return function(next) { return function(action) { if (typeof action === "function") { return action(dispatch, getState); } return next(action); } } };compose
我知道compose是Redux的五大護法之一,可為什么挑在這個時候講它呢?
先不告訴你。
compose在函數式編程中的含義是組合。假如你有一堆函數要依次執行,而且上一個函數的返回結果是下一個函數的參數,我們怎樣寫看起來最裝逼?
const result = a(b(c(d(e("redux")))));
這種寫法讓人一眼就看穿了調用細節,裝逼明顯是不夠的。
我們來看Redux是怎么實現compose的:
export default 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))); }
誒,我看見reduce了,然后...就沒有然后了。
假設我們現在有三個函數:
const funcA = arg => console.log("funcA", arg); const funcB = arg => console.log("funcB", arg); const funcC = arg => console.log("funcC", arg);
執行reduce的第一步返回的accumulator(accumulator是reduce中的概念),結果顯而易見:
(...args) => funcA(funcB(...args));
執行reduce的第二步返回的accumulator,注意到,這時reduce已經執行完了,返回的是一個函數。
(...args) => funcA(funcB(funcC(...args)));
特別提醒:執行compose最終返回的是一個函數。也就是說開發者得這么干compose(a, b, c)()才能讓傳入的函數依次執行。
另外需要注意的是:傳入的函數是從右到左依次執行的。
applyMiddleware廢話少說,先上源代碼:
export default function applyMiddleware(...middlewares) { return createStore => (...args) => { const store = createStore(...args); let dispatch = () => { throw new Error( `Dispatching while constructing your middleware is not allowed. ` + `Other middleware would not be applied to this dispatch.` ); }; const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args), }; const chain = middlewares.map(middleware => middleware(middlewareAPI)); dispatch = compose(...chain)(store.dispatch); return { ...store, dispatch }; } }
還記得中間件閉包好幾層的寫法嗎?現在我們就來一層一層的剝開它。
middlewareAPI是一個對象,正好是傳給第一層中間件函數的參數。執行它,返回的chain是由第二層函數組成的中間件數組。貼一下redux-thunk第二層轉正后的樣子:
function(next) { return function(action) { if (typeof action === "function") { return action(dispatch, getState); } return next(action); } }
中間件第二層函數接收一個next參數,那這個next具體指什么呢?我先透露一下,next是整個Redux中間件機制的題眼,理解了next就可以對Redux中間件的理解達到大徹大悟的化境。
之前我們已經拆解了compose的內部機制,從右到左執行,最右邊的中間件的參數就是store.dispatch,它返回的值就是倒數第二個中間件的next。它返回什么呢?我們再剝一層:
function(action) { if (typeof action === "function") { return action(dispatch, getState); } return next(action); }
別看redux-thunk麻雀雖小,大家發現沒有,第三層函數才是它的邏輯,前面兩層都是配合redux的演出。也就是說呀同學們,除了最后一個中間件的next是原始的dispatch之外,倒數往前的中間件傳入的next都是上一個中間件的邏輯函數。
Redux中間件本質上是將dispatch套上一層自己的邏輯。
最終applyMiddleware里得到的這個dispatch是經過無數中間件精心包裝,植入了自己的邏輯的dispatch。然后用這個臃腫的dispatch覆蓋原有的dispatch,將Store的API返回。
每一個Action就是這樣穿過重重的邏輯代碼才能最后被發射成功。只不過處理異步請求的中間件不再往下走,直到異步請求發生,真正的Action被發射出來,才會走到下一個中間件的邏輯。
構建dispatch過程中禁止執行dispatchmiddlewareAPI中的dispatch為什么是一個拋出錯誤的函數?
我們現在已經知道,applyMiddleware的目的只有一個:用所有中間件組裝成一個超級dispatch,并將它覆蓋原生的dispatch。但是如果超級dispatch還沒組裝完成,就被中間件調用了原生的dispatch,那這游戲別玩了。
所以Redux來了一手掉包。
middlewareAPI初始傳入的dispatch是一個炸彈,中間件的開發者膽敢在頭兩層閉包函數的外層作用域調用dispatch,炸彈就會引爆。而一旦超級dispatch構建完成,這個超級dispatch就會替換掉炸彈。
怎么替換呢?
函數也是引用類型對吧,炸彈dispatch之所以用let定義,就是為了將來修改它的引用地址:
let dispatch = () => { throw new Error( `Dispatching while constructing your middleware is not allowed. ` + `Other middleware would not be applied to this dispatch.` ); }; // ... dispatch = compose(...chain)(store.dispatch);
當然,這是對中間件開發者的約束,如果你只是一個中間件的使用者,這無關緊要。
applyMiddleware的花式調用我們注意到,執行applyMiddleware返回的是一個函數,這個函數有唯一的參數createStore。
WTF?
applyMiddleware不是createStore的參數之一么:
const store = createStore(reducer, applyMiddleware(middleware1, middleware2, middleware3));
怎么createStore也成了applyMiddleware的參數了?
貴圈真亂。
首先我們明確一點,applyMiddleware是一個增強器,增強器是需要改造Store的API的,這樣才能達到增強Store的目的。所以applyMiddleware必須傳入createStore以生成初始的Store。
所以生成一個最終的Store其實可以這樣寫:
const enhancedCreateStore = applyMiddleware(middleware1, middleware2, middleware3)(createStore); const store = enhancedCreateStore(reducer);
那通常的那種寫法,Redux內部是怎么處理的呢?
if (typeof enhancer !== "undefined") { if (typeof enhancer !== "function") { throw new Error("Expected the enhancer to be a function.") } return enhancer(createStore)(reducer, preloadedState) }
上面是createStore源代碼中的一段。
如果enhancer存在并且是一個函數,那么直接傳入createStore執行,再傳入reducer和preloadedState執行(這時候再傳入enhancer就沒完沒了了),然后直接返回。
喵,后面還有好多代碼呢,怎么就返回了?
不,就這么任性。
這么看下來,以下寫法才是正宗的Redux:
const store = applyMiddleware(middleware1, middleware2, middleware3)(createStore)(reducer);
以下寫法只是Redux為開發者準備的語法糖:
const store = createStore(reducer, applyMiddleware(middleware1, middleware2, middleware3));洋蔥圈模型
想必大家都聽說過中間件的洋蔥圈模型,這個比喻非常形象,乍聽上去,啊,好像明白了。但是大家真的對洋蔥圈模型有一個具象化的理解嗎?
假設現在有三個中間件:
const middleware1 = ({ dispatch, getState }) => next => action => { console.log("middleware1 start"); next(action); console.log("middleware1 end"); } const middleware2 = ({ dispatch, getState }) => next => action => { console.log("middleware2 start"); next(action); console.log("middleware2 end"); } const middleware3 = ({ dispatch, getState }) => next => action => { console.log("middleware3 start"); next(action); console.log("middleware3 end"); }
現在將它傳入applyMiddleware:
function reducer(state = {}, action) { console.log("reducer return state"); return state; } const middlewares = [middleware1, middleware2, middleware3]; const store = createStore(reducer, applyMiddleware(...middlewares));
我們看一下打印的結果:
middleware1 start middleware2 start middleware3 start reducer return state middleware3 end middleware2 end middleware1 end
對結果感到驚訝嗎?其實理解函數調用棧的同學就能明白為什么是這樣的結果。reducer執行之前也就是dispatch真正執行之前的日志好理解,dispatch被一層一層包裝,一層一層的深入調用。但是dispatch執行完以后呢?這時候的執行權在調用棧最深的那一層邏輯那里,也就是最接近原始dispatch的邏輯函數那里,所以之后的執行順序是從最深處往上調用。
總的看下來,一個Action的更新Store之旅就像穿過一個洋蔥圈的旅行。一堆中間件簇擁著Action鉆到洋蔥的中心,Action執行自己的使命更新Store后就地圓寂,然后中間件帶著它的遺志再從洋蔥的中心鉆出來。
回看compose其實我解釋上面的打印日志,還有一個關節沒有打通。
記得applyMiddleware的源代碼嗎?內部調用了compose來執行chain。
我們強調過,compose的函數類型參數的執行順序是從右到左的,我相信大家在不少的地方都見到過這樣的表述。但是大家想過沒有,為什么要從右到左執行?原生JavaScript除了實現reduce之外還有一個reduceRight,從左到右執行并沒有什么技術障礙,那么為什么要讓執行順序這么別扭呢?
答案就在上面的打印日志里。
打印日志很好哇,根據傳入的順序執行。對,執行compose是從右到左,但是compose返回的終極dispatch是一層一層從外面包裹的呀,最后一個中間件也就是最左邊的中間件的邏輯,包裹在最外面一層,自然它的日志最先被打印出來。
所以compose被設計成參數從右到左執行,不是有技術障礙,也不是Redux特立獨行,而是其中本來就要經歷一次反轉,compose只有再反轉一次才能將它扭轉過來。
Redux專題一覽考古
實用
中間件
時間旅行
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/97998.html
摘要:光憑一個是無法實現血緣關系疏遠的組件之間的狀態同步的。就是為解決這個問題而生的。,處理動作的派發,相當于架構的。我們的主角是,它也是目前社區最受歡迎的狀態管理框架。專題一覽考古實用中間件時間旅行 本文是『horseshoe·Redux專題』系列文章之一,后續會有更多專題推出來我的 GitHub repo 閱讀完整的專題文章來我的 個人博客 獲得無與倫比的閱讀體驗 React的橫空出世給...
摘要:我們可以為元素添加屬性然后在回調函數中接受該元素在樹中的句柄,該值會作為回調函數的第一個參數返回。使用最常見的用法就是傳入一個對象。單向數據流,比較有序,有便于管理,它隨著視圖庫的開發而被概念化。 面試中問框架,經常會問到一些原理性的東西,明明一直在用,也知道怎么用, 但面試時卻答不上來,也是挺尷尬的,就干脆把react相關的問題查了下資料,再按自己的理解整理了下這些答案。 reac...
摘要:在英文中的意思是有效載荷。有一個動作被發射了顧名思義,替換,這主要是方便開發者調試用的。相同的輸入必須返回相同的輸出,而且不能對外產生副作用。怎么辦呢開發者得手動維護一個訂閱器,才能監聽到狀態變化,從而觸發頁面重新渲染。 本文是『horseshoe·Redux專題』系列文章之一,后續會有更多專題推出來我的 GitHub repo 閱讀完整的專題文章來我的 個人博客 獲得無與倫比的閱讀體...
摘要:希望大家在這浮夸的前端圈里,保持冷靜,堅持每天花分鐘來學習與思考。 今天的React題沒有太多的故事…… 半個月前出了248個Vue的知識點,受到很多朋友的關注,都強烈要求再出多些React相前的面試題,受到大家的邀請,我又找了20多個React的使用者,他們給出了328道React的面試題,由我整理好發給大家,同時發布在了前端面試每日3+1的React專題,希望對大家有所幫助,同時大...
摘要:前言自從上次在掘金發布年山地人的前端完整自學計劃講一個站主山地人的天前端自學故事以來,一眨眼山地人老哥在站做主已經有天了。所以這個體系里的一些框架包括也是山地人年自學計劃的一部分。月底,山地人老哥開啟了的兩個專題。 前言 自從上次在掘金發布【2019年山地人的前端完整自學計劃——講一個B站UP主山地人的40天前端自學故事】 以來,一眨眼山地人老哥在B站做Up主已經有85天了。 時隔一個...
閱讀 2005·2021-09-13 10:23
閱讀 2336·2021-09-02 09:47
閱讀 3798·2021-08-16 11:01
閱讀 1220·2021-07-25 21:37
閱讀 1601·2019-08-30 15:56
閱讀 537·2019-08-30 13:52
閱讀 3132·2019-08-26 10:17
閱讀 2446·2019-08-23 18:17