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