Redux的副作用處理與No-Reducer開發模式

眾所周知,React只是一個用於構建UI的JavaScript庫,並非完整的框架,在代碼的整體結構與組件間的相互通信——這兩塊大型Web項目開發中最關鍵的痛點——選擇了留白。然而值得慶幸的是,Facebook給出了一個官方的半成品解決方案——Flux。

說它是半成品,是因為早在2014年 Facebook 就提出了 Flux 架構的概念,但其後卻一直並沒有一個官方版的完整實現,在社區實現方案中,最有名最成功的當屬Redux

Redux的純函數理想鄉

我們首先來看一下原版的Flux流程:

可以看到,在Flux中,其最大的特點就是數據的單向流動。阮一峰老師的文章中已經講得非常清楚,我在此僅引用要點,Flux的整個流程分為四個部分:

  1. View: 視圖層
  2. Action(動作):視圖層發出的消息(比如mouseClick)
  3. Dispatcher(派發器):用來接收Actions、執行回調函數
  4. Store(數據層):用來存放應用的狀態,一旦發生變動,就提醒Views要更新頁面

Redux作為Flux的實現,加入了一些函數式的編程思想,其中最主要的有三點:

  1. 數據的單一來源:即整個APP盡量不使用內部state,所有的狀態全部源自於Redux
  2. 純函數reducer:處理action並生成新的state,其形式為 newState = f(state,action),不允許任何副作用
  3. 不可變state:只有當state對象被整體替換時候,才會觸發view層的更新。

Redux一經面世,其簡單又優雅的設計便吸粉無數,官方更是自豪的宣稱:

Redux is a predictable state container for JavaScript apps.

這的確是Redux最大的好處,由於任何被action觸發的reducer都是純函數,state狀態的變遷都變得可預測了。也就是說,同一個reducer對於特定action,一定會返回特定的state,不會有任何不確定性因素。reducer函數的定義如下所示:

function reducer(state = initialState, action) {n switch (action.type) {n case SOME_ACTION:n return Object.assign({}, state, {n someKey: action.payloadn })n default:n return staten }n}n

我們看到了函數的入參僅有state和action,即使你使用了combineReducers將多個reducer綁定到一起,你所能看到的也僅僅是自己的state,其他reducer所綁定的state彷彿和你不在一個次元內。

純函數使得Redux的編程模型非常的漂亮和簡潔,我懷疑作者是如此的喜歡這個模型,所以在整整兩年的時間裡,Redux的主要功能竟然幾乎沒演進,也沒有給出任何roadmap,彷彿一切就應該是這個樣子,彷彿一切都是剛剛好。

我也非常喜歡Redux的可預測性,如果你完全按照Redux的理念進行開發,你會發現Redux-devtools是如此的美妙,不僅可以記錄每一步程序的執行狀態,它可以回退到之前任意一個狀態,這是一般調試工具所做不到的。

副作用怎麼辦?

然而非常可惜的是,我們開發的APP無法保證如此的純粹,一個小小的副作用就會使得Redux-devtools失效 。Redux-devtools嚴重依賴事件回放,也就是說當你屏蔽某一個action操作的時候,它實際上是先將整個state恢復為最近確定的state,然後一遍一遍的播放後續的action,生成新的state。這當中如果有任何副作用(非同步操作,外部數據源讀取),都會導致生成不可預測的state。

很多人期待Redux能給出一個終極解決方案,然而Redux似乎把這個皮球又踢回給了社區,我提供一個中間件機制,你們還是另請高明吧。

於是Redux扔給了社區一個叫thunk的例子,這個例子長的什麼樣子呢,反正我第一次見到的時候有點吃驚:

function createThunkMiddleware(extraArgument) {n return ({ dispatch, getState }) => next => action => {n if (typeof action === function) {n return action(dispatch, getState, extraArgument);n }nn return next(action);n };n}nnconst thunk = createThunkMiddleware();nthunk.withExtraArgument = createThunkMiddleware;nnexport default thunk;n

是的,這就是thunk全部的代碼,非常簡潔有力,而使用thunk的非同步actionCreator長成這樣:

export function fetchPostsIfNeeded(subreddit) {n // Note that the function also receives getState()n // which lets you choose what to dispatch next.nn // This is useful for avoiding a network request ifn // a cached value is already available.nn return (dispatch, getState) => {n if (shouldFetchPosts(getState(), subreddit)) {n // Dispatch a thunk from thunk!n return dispatch(fetchPosts(subreddit))n } else {n // Let the calling code know theres nothing to wait for.n return Promise.resolve()n }n }n}n

沒錯,我把dispatch,getState都赤裸的暴露給你,你想取什麼state就取什麼state,dispatch什麼action也隨你心意。什麼,你說功能太簡單了?我們官方不管這個,你可以開發個middleware自己解決。

然而官方對於副作用處理中間件的態度也很是曖昧不清,原則上是支持的,但是在實踐方面卻沒有任何指導,Redux-Saga 作為當前最為優秀的副作用處理中間件,Github上star數已快破萬,Redux官網依舊沒有任何一句表揚的意思。

副作用處理神器,Redux-Saga

在我們團隊的實踐中,選擇了Redux/Redux-Saga加No-Reducer的開發方式。

首先我們回顧一下最普通的Redux/Redux-thunk副作用處理流程:

這種模式除了開發模式簡單,幾乎沒有任何優點,簡單的代價就是開發者需要自己動手處理各種dirty things。

  • 所有的非同步請求全部放在ActionCreator中,大量的非同步回調地獄讓開發者和維護者都心力憔悴
  • 沒有使用任何其他的副作用處理框架,實現諸如Async call/cancel非常的困難,需要開發人員大量手工編碼,經常會出現先發的請求後處理,導致顯示錯誤的舊數據。

在引入Redux-Saga後,上述兩點問題得到了完美解決,處理流程變為

Redux-Saga充分利用了ES6的Generator特性,一切非同步調用都可以通過yield交給Redux-Saga去處理,並在非同步代碼執行完成後返回yield處繼續執行,從此和回調形式的非同步調用說再見,同時支持try/catch語法,可以更方便的對代碼塊進行異常捕獲。為了不使這篇文章變成Redux-Saga的使用技巧集錦,我對Redux-Saga不再做更多介紹,只是在這裡真心再誇一句,Redux-Saga完美解決了我們所面臨所有的非同步處理/競爭條件的難題。

No-Reducer開發模式

在上面這種模式中,我們最開始的時候是相當滿意的,對Action的類型進行嚴格區分,有副作用的一律走Redux-Saga,純函數一律走Reducer。當然還是有美中不足的地方,就是Redux的事件定義機制過於「繁瑣」了,如果是一個簡單的功能,如模態表單的彈出/隱藏,都需要定義Action類型,有可能定義類型的時間,功能代碼都寫好了。

這種簡單Action類型定義過多的話,上百的Action類型也會讓後來者看得眼花繚亂,不利於代碼的維護。為了解決這個問題,我們使用了No-Reducer開發模式:

  1. 簡化reducer,僅接收和處理SET_STATE、REPLACE_STATE兩種Action,且最終的狀態變換一定要調用這兩個方法之一。
  2. 簡單的Action不設立單獨的Action類型,如彈出窗口開關,簡單的加減等Action,直接調用SET_STATE完成狀態變換。
  3. 複雜的Action都需要有單獨的類型定義,如需要從後台獲取數據,或者處理步驟超過10行的Action。並且處理函數要求在Saga中獨立註冊,以供復用或組合。

採用這種開發模式後,終於使得開發人員從Action類型定義的地獄中解脫出來,大部分組件的Action類型可以控制在10個以內。

簡化後的noReducer代碼如下:

function noReducer(state = initialState, action) {n switch (action.type) {n case SET_STATE:n return Object.assign({}, state, action.state)n case REPLACE_STATE:n return action.staten default:n return staten }n}n

在noReducer中,我們只接受兩種數據類型,SET_STATE和REPLACE_STATE,使用方法類似於React組件內部的setState與state初始化,分別對應將新的state狀態merge到現存的state對象中,以及直接替換當前state,在絕大多數情況下,我們僅需要使用SET_STATE,因為initialState的存在,我們一般不需要對state進行手動初始化。

No-Reducer處理流程圖如下:

可以看到,所有原本屬於reducer的工作都在ActionCreator和Redux-Saga中完成了,reducer變得名存實亡。

Redux state 即內部state

仔細回想一下,我們使用了No-Reducer開發模式後,彷彿真的回到了起點,在React組件內部,我們不就是這麼管理state的嗎,在函數中處理處理state,然後使用setState刷新頁面。

本質上,No-Reducer開發模式就是一個增強版的內部state,setState模式。前文說過,Redux推崇數據單一來源,Redux的state原本就是被設計取代組件內部state的,所有組件渲染需要的state都應該被放入Redux中集中管理。而SET_STATE的表現和setState幾乎一致,可以讓開發者更容易接收Redux,並且付出很小的代價將內部state遷移到Redux中統一管理。

將state從React內部移到外部的好處非常明顯,不管是測試、管理還是狀態共享,一個統一的狀態來源,都比一個需要手動層層傳遞,並且零碎分布於各個組件要強得多。當然我們還需要一個好的工具來組織化外部state,比如反應出state與真實組件對應的層次結構,而不是扁平的展現在開發者面前,以及更好的狀態共享方案。這些功能都在我們開發的 Redux-Arena 框架的RoadMap中。

我本人非常喜歡Redux,它的函數式思想深刻的影響了我的編程理念,全局state和不可變state機制如果能得以貫徹,前端開發工作會變得更加簡單和高效。僅有的不滿大概是Redux已經很久沒有功能上的進步,以及對副作用處理的漠不關心。

最後再安利一波吧,我們的 Redux-Arena 框架就是在No-Reducer開發模式下誕生的。它的設計目標是實現Redux/Redux-Saga與React更好的整合。目前已經實現了Redux的模塊化,並提供React-Router兼容高階組件,可以自動清理被卸載的React組件綁定的Redux信息,下一個版本會提供Virtual ReducerKey狀態共享方案,使父子組件間的狀態共享更加方便,敬請期待。


推薦閱讀:

六神丸有嘔吐的副作用嗎?
奧利司他有哪些副作用?
碧緹福冷光牙齒美白儀有副作用嗎?
放療副作用深度科普之一:概述篇
腫瘤患者的骨髓抑制及家庭自我護理

TAG:Redux | React | 副作用 |