函數(shù)式編程在Redux/React中的應(yīng)用
本文簡述了軟件復(fù)雜度問題及應(yīng)對策略:抽象和組合;展示了抽象和組合在函數(shù)式編程中的應(yīng)用;并展示了Redux/React在解決前端狀態(tài)管理的復(fù)雜度方面對上述理論的實(shí)踐。這其中包括了一段有趣的Redux推導(dǎo)。
軟件復(fù)雜度及其應(yīng)對策略
軟件復(fù)雜度
軟件的首要技術(shù)使命是管理復(fù)雜度。——代碼大全
在軟件開發(fā)過程中,隨著需求的變化和系統(tǒng)規(guī)模的增大,我們的項目不可避免地會趨于復(fù)雜。如何對軟件復(fù)雜度及其增長速率進(jìn)行有效控制,便成為一個日益突出的問題。下面介紹兩種控制復(fù)雜度的有效策略。
對應(yīng)策略
抽象
世界的復(fù)雜、多變和人腦處理問題能力的有限性,要求我們在認(rèn)識世界時對其做簡化,提取出一般化和共性的概念,形成理論和模型,然后反過來指導(dǎo)我們改造世界。而一般化的過程即抽象的過程,抽象思維使我們忽略不同事物的細(xì)節(jié)差異,抓住它們的本質(zhì),并提出解決本質(zhì)問題的普適策略。
例如,范疇論將世界抽象為對象和對象之間的聯(lián)系,Linux 將所有I/O接口都抽象為文件,Redux將所有事件抽象為action。
組合
組合是另一種處理復(fù)雜事物的有效策略。通過簡單概念的組合可以構(gòu)造出復(fù)雜的概念;通過將復(fù)雜任務(wù)拆分為多個低耦合度的簡單的子任務(wù),我們可以對各子任務(wù)分而治之;各子任務(wù)解決后,將它們重新組合起來,整個任務(wù)便得以解決。
軟件開發(fā)的過程,本質(zhì)上也是人們認(rèn)識和改造世界的一種活動,所以也可以借助抽象和組合來處理復(fù)雜的任務(wù)。
抽象與組合在函數(shù)式編程中的應(yīng)用
函數(shù)式編程是相對于命令式編程而言的。命令式編程依賴數(shù)據(jù)的變化來管理狀態(tài)變化,而函數(shù)式編程為克服數(shù)據(jù)變化帶來的狀態(tài)管理的復(fù)雜性,限制數(shù)據(jù)為不可變的,其選擇使用流式操作來進(jìn)行狀態(tài)管理。而流式操作以函數(shù)為基本的操作單元,通過對函數(shù)的抽象和組合來完成整個任務(wù)。下面對抽象和組合在函數(shù)式編程中的應(yīng)用進(jìn)行詳細(xì)的講解。
高階函數(shù)的抽象
一種功能強(qiáng)大的語言,需要能為公共的模式命名,建立抽象,然后直接在抽象的層次上工作。
如果函數(shù)只能以數(shù)值或?qū)ο鬄閰?shù),將會嚴(yán)重限制人們建立抽象的能力。經(jīng)常會有一些同樣的設(shè)計模式能用于若干不同的過程。為了將這種模式描述為相應(yīng)的概念,就需要構(gòu)造出這樣的函數(shù),使其以函數(shù)作為參數(shù),或者將函數(shù)作為返回值。這類能操作函數(shù)的函數(shù)稱為高階函數(shù)。
在進(jìn)行序列操作時,我們抽象出了三類基本操作:map、filter 和 reduce ??梢酝ㄟ^向這三個抽象出來的高階函數(shù)注入具體的函數(shù),生成處理具體問題的函數(shù);進(jìn)一步,通過組合這些生成的具體的函數(shù),幾乎可以解決所有序列相關(guān)的問題。以 map 為例,其定義了一大類相似序列的操作:對序列中每個元素進(jìn)行轉(zhuǎn)換。至于如何轉(zhuǎn)換,需要向 map 傳入一個具體的轉(zhuǎn)換函數(shù)進(jìn)行具體化。這些抽象出來的高階函數(shù)相當(dāng)于具有某類功能的通用型機(jī)器,而傳入的具體函數(shù)相當(dāng)于特殊零件,通用機(jī)器配上具體零件就可以應(yīng)用于屬于該大類下的各種具體場景了。
map 的重要性不僅體現(xiàn)在它代表了一種公共的模式,還體現(xiàn)在它建立了一種處理序列的高層抽象。迭代操作將人們的注意力吸引到對于序列中逐個元素的處理上,引入 map 抑制了對這種細(xì)節(jié)層面上的關(guān)注,強(qiáng)調(diào)的是從源序列到目標(biāo)序列的變換。這兩種定義形式之間的差異,并不在于計算機(jī)會執(zhí)行不同的計算過程,而在于我們對同一種操作的不同思考方式。從作用上看,map 幫我們建立了一層抽象屏障,將序列轉(zhuǎn)換的函數(shù)實(shí)現(xiàn),與如何提取序列中元素以及組合結(jié)果的細(xì)節(jié)隔離開。這種抽象也提供了新的靈活性,使我們有可能在保持從序列到序列的變換操作框架的同時,改變序列實(shí)現(xiàn)的底層細(xì)節(jié)。
例如,我們有一個序列:
- const list = [9, 5, 2, 7]
若對序列中的每個元素加 1:
- map(a => a + 1, list) //=> [10, 6, 3, 8]
若對序列中的每個元素平方:
- map(a => a * a, list) //=> [81, 25, 4, 49]
我們只需向 map 傳入具體的轉(zhuǎn)換函數(shù),map 便會自動將函數(shù)映射到序列的的每個元素。
高階函數(shù)的組合
高階函數(shù)使我們可以顯式地使用程序設(shè)計元素描述過程(函數(shù))的抽象,并能像操作其它元素一樣去操作它們。這讓我們可以對函數(shù)進(jìn)行組合,將多個簡單子函數(shù)組合成一個處理復(fù)雜任務(wù)的函數(shù)。下面對高階函數(shù)的組合進(jìn)行舉例說明。
現(xiàn)有一份某公司雇員某月的考核表,我們想統(tǒng)計所有到店餐飲部開發(fā)人員該月完成的任務(wù)總數(shù),假設(shè)員工七月績效結(jié)構(gòu)如下:
- [{
- name: 'Pony',
- level: 'p2.1',
- segment: '到餐'
- tasks: 16,
- month: '201707',
- type: 'RD',
- ...
- }, {
- name: 'Jack',
- level: 'p2.2',
- segment: '外賣'
- tasks: 29,
- month: '201707',
- type: 'QA',
- ...
- }
- ...
- ]
我們可以這樣做:
- const totalTaskCount = compose(
- reduce(sum, 0), // 4. 計算所有 RD 任務(wù)總和
- map(person => person.tasks), // 3. 提取每個 RD 的任務(wù)數(shù)
- filter(person => person.type === 'RD'), // 2. 篩選出到餐部門中的RD
- filter(person => person.segment === '到餐') // 1. 篩選出到餐部門的員工
- )
上述代碼中,compose 是用來做函數(shù)組合的,上一個函數(shù)的輸出作為下一個函數(shù)的輸入。類似于流水線及組成流水線的工作臺。每個被組合的函數(shù)相當(dāng)于流水線上的工作臺,每個工作臺對傳過來的工件進(jìn)行加工、篩選等操作,然后輸出給下一個工作臺進(jìn)行處理。
compose 調(diào)用順序為從右向左(自下而上),Ramda 提供了另一個與之對應(yīng)的API:pipe,其調(diào)用順序為從左向右。compose意為組合,pipe意為管道、流,其實(shí)流是一種縱向的函數(shù)組合。
計算到餐RD完成任務(wù)總數(shù)示意圖如下所示:
通過上節(jié)map示例和本節(jié)的計算到餐RD完成任務(wù)總數(shù)的示例,我們可以看到利用高階函數(shù)進(jìn)行抽象和組合的強(qiáng)大和簡潔之處。這種通用模式(模塊)+ "具體函數(shù)"組合的模式,顯示了通用模塊的普適性和處理具體問題時的靈活性。
上面講了很多高階函數(shù)的優(yōu)勢和實(shí)踐,然而一門語言如何才能支持高階函數(shù)呢?
通常,程序設(shè)計語言總會對基本元素的可能使用方式進(jìn)行限制。帶有最少限制的元素被稱為一等公民,包括的 "權(quán)利或者特權(quán)" 如下所示:
- 可以使用變量命名;
- 可以提供給函數(shù)作為參數(shù);
- 可以由函數(shù)作為結(jié)果返回;
- 可以包含在數(shù)據(jù)結(jié)構(gòu)中;
幸運(yùn)的是在JavaScript中,函數(shù)被看作是一等公民,也即我們可以在JavaScript中像使用普通對象一樣使用高階函數(shù)進(jìn)行編程。
流式操作
由上述過程我們得到了一種新的模式——數(shù)據(jù)流。信號處理工程師可以很自然地用流過一些級聯(lián)的處理模塊信號的方式來描述這一過程。例如我們輸入公司全員月度考核信息作為信號,首先會流過兩個過濾器,將所有不符合要求的數(shù)據(jù)過濾掉,這樣得到的信號又通過一個映射,這是一個 "轉(zhuǎn)換裝置",它將完整的員工對象轉(zhuǎn)換為對應(yīng)的任務(wù)信息。這一映射的輸出被饋入一個累加器,該裝置用 sum 將所有的元素組合起來,以初始的0開始。
要組織好這些過程,最關(guān)鍵的是將注意力集中在處理過程中從一個步驟流向下一個步驟的"信號"。如果我們用序列來表示這些信號,就可以利用序列操作實(shí)現(xiàn)每步處理。
或許因為序列操作模式非常具有一般化的性質(zhì),于是人們發(fā)明了一門專門處理序列的語言Lisp(LISt Processor)……
將程序表示為針對序列的操作,這樣做的價值就在于能幫助我們得到模塊化的程序設(shè)計,也就是說,得到由一些比較獨(dú)立的片段的組合構(gòu)成的設(shè)計。通過提供一個標(biāo)準(zhǔn)部件的庫,并使這些部件都有著一些能以各種靈活方式相互連接的約定接口,將能進(jìn)一步推動人們?nèi)プ瞿K化的設(shè)計。
用流式操作進(jìn)行狀態(tài)管理
在前面,我們已經(jīng)看到了組合和抽象在克服大型系統(tǒng)復(fù)雜性方面所起的作用。但還需要一些能夠在整體架構(gòu)層面幫助我們構(gòu)造起模塊化的大型系統(tǒng)的策略。
目前有兩種比較流行的組織策略:面向?qū)ο蠛土魇讲僮鳌?/p>
面向?qū)ο蠼M織策略將注意力集中在對象上,將一個大型系統(tǒng)看成一大批對象,它們的狀態(tài)和行為可能隨著時間的進(jìn)展而不斷變化。流式操作組織策略將注意力集中在流過系統(tǒng)的信息流上,很像電子工程師觀察一個信號處理系統(tǒng)。
在利用面向?qū)ο竽J侥M真實(shí)世界中的現(xiàn)象時,我們用具有局部狀態(tài)的計算對象去模擬真實(shí)世界里具有局部狀態(tài)的對象;用計算機(jī)里面隨著時間的變化去表示真實(shí)世界里隨著時間的變化;在計算機(jī)里,被模擬對象隨著時間的變化是通過對那些模擬對象中局部變量的賦值實(shí)現(xiàn)的。
我們必須讓相應(yīng)的模型隨著時間變化,以便去模擬真實(shí)世界中的現(xiàn)象嗎?答案是否定的。如果以數(shù)學(xué)函數(shù)的方式考慮這些問題,我們可以將一個量 x 隨時間而變化的行為,描述為一個時間的函數(shù) x(t)。如果我們集中關(guān)注的是一個個時刻的 x,可以將它看做一個變化著的量。如果關(guān)注的是這些值的整個時間史,那么就不需要強(qiáng)調(diào)其中的變化——這一函數(shù)本身并沒有變化。
如果用離散的步長去度量時間,就可以用一個(可能無窮的)序列來模擬變化,以這種序列表示被模擬系統(tǒng)隨著時間變化的歷史。為此,我們需要引進(jìn)一種稱為流的新數(shù)據(jù)結(jié)構(gòu)。從抽象的角度看,一個流也是一個序列(無窮序列)。
流處理使我們可以模擬一些包含狀態(tài)的系統(tǒng),但卻不需要賦值或者變動數(shù)據(jù),能避免由于引進(jìn)了賦值而帶來的內(nèi)在缺陷。
例如在前端開發(fā)中,一般會用對象模型(DOM)來模擬和直接操控網(wǎng)頁,隨著與用戶不斷交互,網(wǎng)頁的局部狀態(tài)不斷被修改,其中的行為也會隨時間不斷變化。隨著時間的累積,我們頁面狀態(tài)管理變得愈加復(fù)雜,以致于最終我們可能自己也不知道網(wǎng)頁當(dāng)前的狀態(tài)和行為。
為了克服對象模型隨時間變化帶來的狀態(tài)管理困境,我們引入了 Redux,也就是上面提到的流處理模式,將頁面狀態(tài) state 看作時間的函數(shù) state = state(t) -> state = stateF(t),因為狀態(tài)的變化是離散的,所以我們也可以寫成 stateF(n) 。通過提取 state 并顯式地增加時間維度,我們將網(wǎng)頁的對象模型轉(zhuǎn)變?yōu)榱魈幚砟P停?[state] 序列表示網(wǎng)頁隨著時間變化的狀態(tài)。
由于 state 可以看做整個時間軸上的無窮(具有延時)序列,并且我們在之前已經(jīng)構(gòu)造起了對序列進(jìn)行操作的功能強(qiáng)大的抽象機(jī)制,所以可以利用這些序列操作函數(shù)處理 state ,這里我們用到的是 reduce 。
函數(shù)式編程在Redux/React中的應(yīng)用
從reduce到Redux
reduce
reduce 是對列表的迭代操作的抽象,map 和 filter 都可以基于 reduce 進(jìn)行實(shí)現(xiàn)。Redux借鑒了reduce 的思想,是 reduce 在時間流處理上的一種特殊應(yīng)用。接下來我們展示Redux是怎樣由 reduce 一步步推導(dǎo)出來的。
首先看一下 reduce 的類型簽名:
- reduce :: ((a, b) -> a) -> a -> [b] -> a
- reduce :: (reducer, initialValue, list) -> result
- reducer :: (a, b) -> a
- initialValue :: a
- list :: [b]
- result :: a
上述類型簽名采用的是Hindley-Milner 類型系統(tǒng),接觸過Haskell的的同學(xué)對此會比較熟悉。其中 :: 左側(cè)部分為函數(shù)或參數(shù)名稱,右側(cè)為該函數(shù)或參數(shù)的類型。
reduce 接受三個參數(shù):累積器 reducer ,累積初始值 initialValue,待累積列表 list 。我們迭代遍歷列表的元素,利用累積器reducer 對累積值和列表當(dāng)前元素進(jìn)行累積操作,reducer 輸出新累積值作為下次累積操作的輸入。依次循環(huán)迭代,直到遍歷結(jié)束,將此時的累積值作為 reduce 最終累積結(jié)果輸出。
reduce 在某些編程語言中也被稱為 foldl。中文翻譯有時也被稱為折疊、歸約等。如果將列表看做是一把展開的扇子,列表中的每個元素看做每根扇骨,則 reduce 的過程也即扇子從左到右不斷折疊(歸約、累積)的過程。當(dāng)扇子完全合上,一次折疊也即完成。當(dāng)然,折疊順序也可以從右向左進(jìn)行,即為 reduceRight 或foldr。
reduce 代碼實(shí)現(xiàn)如下:
- const reduce = (reducer, initialValue, list) => {
- let acc = initialValue;
- let val;
- for(let i = 0; i < list.length; i++) {
- val = list[i];
- acc = reducer(acc, val);
- }
- return acc;
- };
例如,我們想對一個數(shù)字列表 [2, 3, 4] 進(jìn)行累加操作(初始值為 1 ),可以表示為:
reduce((a, b) => a + b, 1, [2, 3, 4])
示意圖如下所示:
介紹完 reduce 的基本概念,接下來展示如何由 reduce 一步步推導(dǎo)出 Redux,以及 Redux 各部分與reduce 的對應(yīng)關(guān)系。
Redux
首先定義 Redux 的類型簽名:
- redux :: ((state, action) -> state) -> initialState -> [action] -> state
- redux :: (reducer, initialState, stream) -> result
- reducer :: (state, action) -> state
- initialState :: state
- list :: [action]
- result :: state
將 reduce 參數(shù)的名稱變換一下,便得到Redux的類型簽名。從類型簽名看,Redux參數(shù)包含 reducer 函數(shù),state初始值 initialState ,和一個以 action 為元素的時間流列表 stream :: [action];返回值為最終的狀態(tài) state。
Redux初步實(shí)現(xiàn)
下面看一下Redux的初步實(shí)現(xiàn):
- const redux = (reducer, initialState, stream) => {
- let state = initialState;
- let action;
- for(let i = 0; i < stream.length; i++) {
- action = stream[i];
- state = reducer(state, action);
- }
- return state;
- }
首先設(shè)置Redux state 的初始值 initialState,stream 代表基于時間的事件流列表,action = stream[i] 代表事件流上某個時間點(diǎn)發(fā)生的一次 action。每次 for 循環(huán),我們將當(dāng)前的狀態(tài) state 和action 傳給 reducer 函數(shù),根據(jù)本次 action 對當(dāng)前 state 進(jìn)行更新,產(chǎn)生新的 state。新的state 作為下次 action 發(fā)生時的 state 參與狀態(tài)更新。
Redux基本原理其實(shí)已經(jīng)講完了,Redux的各個概念如:reducer 函數(shù)、state、 stream :: [action] 也是和 reduce 一一對應(yīng)的。不同之處在于,redux 中的列表 stream,是一個隨時間不斷生成的***長的action 動作列表,而 reduce 中的列表是一個普通的 list。
等一下,上述Redux實(shí)現(xiàn)貌似缺了些什么……
是的,在Redux中,狀態(tài)的改變和獲取是通過兩個函數(shù)來操作的:dispatch、getState,接下來我們將這兩個函數(shù)添加進(jìn)去。
Redux優(yōu)化實(shí)現(xiàn)
- const redux = (reducer, initialState, stream) => {
- let currentState = initialState;
- let action;
- const dispatch = action => {
- currentState = reducer(currentState, action);
- };
- const getState = () => currentState;
- for(i = 0; i < stream.length; i++) {
- action = stream[i];
- dispatch(action);
- }
- return state; // the end of the world :)
- }
這樣我們就可以通過 dispatch(action) 來更新當(dāng)前的狀態(tài),通過 getState 也可以拿到當(dāng)前的狀態(tài)。
但是還是感覺不太對?
在上述實(shí)現(xiàn)中,stream 并不是現(xiàn)實(shí)中的事件流,只是普通的列表而已,dispatch 和 getState 接口也并沒有暴露給外部,同時在Redux***還有一個 return state ,既然說過 stream 是一個***長的列表,那return state 貌似沒有什么意義。
好吧,上述兩次Redux代碼實(shí)現(xiàn),其實(shí)都是對Redux原理的說明,下面我們來真正實(shí)現(xiàn)一個現(xiàn)實(shí)中可運(yùn)行的最小Redux代碼片段。
Redux可用的最小實(shí)現(xiàn)
- const redux = (reducer, initialState) => {
- let currentState = initialState;
- const dispatch = action => {
- currentState = reducer(currentState, action);
- };
- const getState = () => currentState;
- return ({
- dispatch,
- getState,
- });
- };
- const store = redux(reducer, initialState);
- const action = { type, payload };
- store.dispatch(action);
- store.getState();
Yes! 我們將 stream 從Redux函數(shù)中抽離出來,或者說是從電腦屏幕上抽取到現(xiàn)實(shí)世界中了。
我們首先使用 reducer 和 initialState 初始化 redux 為 store;然后現(xiàn)實(shí)中每次事件發(fā)生時,我們通過store.dispatch(action) 更新store中狀態(tài);同時通過 store.getState() 來獲取 store 的當(dāng)前狀態(tài)。
等等,這怎么聽著像是面向?qū)ο蟮木幊谭绞?,對象中包含私有變量:currentState 和操作私有變量的方法:dispatch 和 getState,偽代碼如下所示:
- const store = {
- private currentState: initialState,
- public dispatch: (action) => { currentState = reducer(currentState, action)},
- public getState: () => currentState,
- }
是的,從這個角度講,我們確實(shí)是用了函數(shù)式的過程實(shí)現(xiàn)了一個面向?qū)ο蟮母拍睢?/p>
如果你再仔細(xì)看的話,我們用閉包(編程領(lǐng)域的閉包,與集合意義上的閉包不同)實(shí)現(xiàn)的這個對象,雖然***的Redux實(shí)現(xiàn)返回的是形式為 { dispatch, getState } store 對象,但 dispatch 和 getState 捕獲了Redux內(nèi)部創(chuàng)建的 currentState,因此形成了閉包。
Redux的運(yùn)作過程如下所示:
Redux 和 reduce 的聯(lián)系與區(qū)別
我們來總結(jié)一下 Redux 和 reduce 的聯(lián)系與區(qū)別。
相同點(diǎn):
- reduce和Redux都是對數(shù)據(jù)流進(jìn)行fold(折疊、歸約);
- 兩者都包含一個累積器(reducer)((a, b) -> a VS (state, action) -> state )和初始值(initialValue VS initialState ),兩者都接受一個抽象意義上的列表(list VS stream )。
不同點(diǎn):
- reduce:接收一個有限長度的普通列表作為參數(shù),對列表中的元素從前往后依次累積,并輸出最終的累積結(jié)果。
- Redux:由于基于時間的事件流是一個***長的抽象列表,我們無法顯式地將事件流作為參數(shù)傳給Redux,也無法返回最終的累積結(jié)果(事件流***長)。所以我們將事件流抽離出來,通過 dispatch 主動地向 reducer 累積器 push action,通過getState 觀察當(dāng)前的累積值(中間的累積過程)。
- 從冷、熱信號的角度看,reduce 的輸入相當(dāng)于冷信號,累積器需要主動拉取(pull)輸入列表中的元素進(jìn)行累積;而Redux的輸入(事件流)相當(dāng)于熱信號,需要外部主動調(diào)用 dispatch(action) 將當(dāng)前元素push給累積器。
由上可知,Redux將所有的事件都抽象為 action,無論是用戶點(diǎn)擊、Ajax請求還是頁面刷新,只要有新的事件發(fā)生,我們就會 dispatch 一個 action 給 reducer,并結(jié)合上一次的狀態(tài)計算出本次狀態(tài)。抽象出來的統(tǒng)一的事件接口,簡化了處理事件的復(fù)雜度。
Redux還規(guī)范了事件流——單向事件流,事件 action 只能由 dispatch 函數(shù)派發(fā),并且只能通過 reducer更新系統(tǒng)(網(wǎng)頁)的狀態(tài) state,然后等待下一次事件。這種單向事件流機(jī)制能夠進(jìn)一步簡化事件管理的復(fù)雜度,并且有較好的擴(kuò)展性,可以在事件流動過程中插入 middleware,比如日志記錄、thunk、異步處理等,進(jìn)而大大增強(qiáng)事件處理的靈活性。
Redux 的增強(qiáng):Transduce與Redux Middleware
transduce 作為增強(qiáng)版的 reduce,是在 Clojure 中***引入的。transduce 相當(dāng)于 compose 和 reduce的組合,相對于 reduce 改進(jìn)之處為:列表中的每個元素在放入累積器之前,先對其進(jìn)行一系列的處理。這樣做的好處是能同時降低代碼的時間復(fù)雜度和空間復(fù)雜度。
假設(shè)有一個長度為n的列表,傳統(tǒng)列表處理的做法是先用 compose 組合一系列列表處理函數(shù)對列表進(jìn)行轉(zhuǎn)換處理,***對處理好的列表進(jìn)行歸約(reduce)。假設(shè)我們組合了 m 個列表處理函數(shù),加上***一次reduce,時間復(fù)雜度為 n * (m + 1);而使用 transduce 只需要一次循環(huán),所以時間復(fù)雜度為 n 。由于compose 的每個處理函數(shù)都會產(chǎn)生中間結(jié)果,且這些中間結(jié)果有時會占用很大的內(nèi)存,而 transduce 邊轉(zhuǎn)換邊累積,沒有中間結(jié)果產(chǎn)生,所以空間復(fù)雜度也得到了有效的控制。
我們也可以對Redux進(jìn)行類似地增強(qiáng)優(yōu)化,每次 dispatch(action) 時,我們先根據(jù) action 進(jìn)行一系列操作,***傳給 reducer 函數(shù)進(jìn)行真正的狀態(tài)更新。這就是上文提到的Redux middleware。Redux是一個功能和擴(kuò)展性非常強(qiáng)的狀態(tài)管理庫,而圍繞Redux產(chǎn)生的一系列優(yōu)秀的middlewares讓Redux/React 形成了一個強(qiáng)大的前端生態(tài)系統(tǒng)。個人認(rèn)為Redux/React自身良好的架構(gòu)、先進(jìn)的理念,加上一系列優(yōu)秀的第三方插件的支持,是React/Redux成功的關(guān)鍵所在。
純函數(shù)在React中的應(yīng)用
Redux可以用作React的數(shù)據(jù)管理(數(shù)據(jù)源),React接受Redux輸出的state,然后將其轉(zhuǎn)換為瀏覽器中的具體頁面展示出來:
view = React(state)
由上可知,我們可以將React看作輸入為state,輸出為view的“純”函數(shù)。下面講解純函數(shù)的概念、優(yōu)點(diǎn),及其在React中的應(yīng)用。
純函數(shù)的定義:相同的輸入,永遠(yuǎn)會得到相同的輸出,并且沒有副作用。
純函數(shù)的運(yùn)算既不受外部環(huán)境和內(nèi)部不確定性因素的影響,也不會影響外部環(huán)境。輸出只與輸入有關(guān)。
由此可得純函數(shù)的一些優(yōu)點(diǎn):可緩存、引用透明、可等式推導(dǎo)、可預(yù)測、單測友好、易于并發(fā)操作等。
其實(shí)函數(shù)式編程中的純函數(shù)指的是數(shù)學(xué)意義上的函數(shù),數(shù)學(xué)中函數(shù)定義為:
函數(shù)是不同數(shù)值之間的特殊關(guān)系:每一個輸入值返回且只返回一個輸出值。
從集合的角度講,函數(shù)分為三部分:定義域和值域,以及定義域到值域的映射。函數(shù)調(diào)用(運(yùn)算)的過程即定義域到值域映射的過程。
如果忽略中間的計算過程,從對象的角度看,函數(shù)可以看做是鍵值對映射,輸入?yún)?shù)為鍵,輸出參數(shù)為鍵對應(yīng)的值。如果一段代碼可以替換為其執(zhí)行結(jié)果,而且是在不改變整個程序行為的前提下替換的,我們就說這段代碼是引用透明的。
由于純函數(shù)相同的輸入總是返回相同的輸出,我們認(rèn)為純函數(shù)是引用透明的。
純函數(shù)的緩存便是引用透明的一個典型應(yīng)用,我們將被調(diào)用過的參數(shù)及其輸出結(jié)果作為鍵值對緩存起來,當(dāng)下次調(diào)用該函數(shù)時,先查看該參數(shù)是否被緩存過,如果是,則直接取出緩存中該鍵對應(yīng)的值作為調(diào)用結(jié)果返回。
緩存技術(shù)在做耗時較長的函數(shù)調(diào)用時比較有用,比如GPU在做大型3D游戲畫面渲染時,會對計算時間較長的渲染做緩存,從而增強(qiáng)畫面的流暢度。網(wǎng)頁中的DOM操作也是非常耗時的,而React組件本身也是純函數(shù),所以React對 state 可以進(jìn)行緩存,如果state沒有變化,就還用之前的網(wǎng)頁,頁面不需要重新渲染。
帶有緩存的最終 React-Redux 框架如下所示:
總結(jié)
我們從產(chǎn)生軟件復(fù)雜度的原因出發(fā),從方法層面上講了控制代碼復(fù)雜度的兩種基本方式:抽象和組合,利用處理列表的高階函數(shù)(map、filter、reduce、compose)對抽象和組合進(jìn)行了舉例解釋。
然后從整體架構(gòu)層面上講了應(yīng)對復(fù)雜度的策略:面向?qū)ο蠛土魇教幚恚治隽藘烧叩幕纠砟睿约傲魇教幚碓跔顟B(tài)管理方面的優(yōu)勢,引申出基于時間的抽象事件流。
然后我們展示了如何從列表處理方法 reduce 推導(dǎo)出可用的事件流處理框架Redux,并將 reduce 的加強(qiáng)版transduce 與Redux的 middleware 做了類比。
***講了純函數(shù)在 react/redux 框架中的應(yīng)用:將頁面渲染抽象為純函數(shù),利用純函數(shù)進(jìn)行緩存等。
貫穿文章始終的是抽象、組合、函數(shù)式編程以及流式處理。希望通過本文讓大家對軟件開發(fā)的一些基本理念及其應(yīng)用有所了解。從 reduce 推導(dǎo)出Redux的過程非常有趣,感興趣的同學(xué)可以多看一下。
【本文為51CTO專欄機(jī)構(gòu)“美團(tuán)點(diǎn)評技術(shù)團(tuán)隊”的原創(chuàng)稿件,轉(zhuǎn)載請通過微信公眾號聯(lián)系機(jī)構(gòu)獲取授權(quán)】