自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

函數(shù)式編程在Redux/React中的應(yīng)用

開(kāi)發(fā) 開(kāi)發(fā)工具 后端
本文簡(jiǎn)述了軟件復(fù)雜度問(wèn)題及應(yīng)對(duì)策略:抽象和組合;展示了抽象和組合在函數(shù)式編程中的應(yīng)用;并展示了Redux/React在解決前端狀態(tài)管理的復(fù)雜度方面對(duì)上述理論的實(shí)踐。這其中包括了一段有趣的Redux推導(dǎo)。

[[206629]]

本文簡(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è)序列:

  1. const list = [9, 5, 2, 7] 

若對(duì)序列中的每個(gè)元素加 1:

  1. map(a => a + 1, list) //=> [10, 6, 3, 8] 

若對(duì)序列中的每個(gè)元素平方:

  1. 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)如下:

  1. [{ 
  2.   name'Pony'
  3.   level'p2.1'
  4.   segment: '到餐' 
  5.   tasks: 16, 
  6.   month'201707'
  7.   type: 'RD'
  8.   ... 
  9. }, { 
  10.   name'Jack'
  11.   level'p2.2'
  12.   segment: '外賣(mài)' 
  13.   tasks: 29, 
  14.   month'201707'
  15.   type: 'QA'
  16.   ... 
  17. ... 

我們可以這樣做:

  1. const totalTaskCount = compose( 
  2.   reduce(sum, 0),                              // 4. 計(jì)算所有 RD 任務(wù)總和 
  3.   map(person => person.tasks),                 // 3. 提取每個(gè) RD 的任務(wù)數(shù) 
  4.   filter(person => person.type === 'RD'),      // 2. 篩選出到餐部門(mén)中的RD 
  5.   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)型簽名:

  1. reduce :: ((a, b) -> a) -> a -> [b] -> a 
  2.  
  3. reduce :: (reducer, initialValue, list) -> result 
  4. reducer :: (a, b) -> a 
  5. initialValue :: a 
  6. list :: [b] 
  7. 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)如下:

  1. const reduce = (reducer, initialValue, list) => { 
  2.   let acc = initialValue; 
  3.   let val; 
  4.   for(let i = 0; i < list.length; i++) { 
  5.     val = list[i]; 
  6.     acc = reducer(acc, val); 
  7.   } 
  8.   return acc; 
  9. }; 

例如,我們想對(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)型簽名:

  1. redux :: ((state, action) -> state) -> initialState -> [action] -> state 
  2. redux :: (reducer, initialState, stream) -> result 
  3.  
  4. reducer :: (state, action) -> state 
  5. initialState :: state 
  6. list :: [action
  7. 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):

  1. const redux = (reducer, initialState, stream) => { 
  2.   let state = initialState; 
  3.   let action
  4.   for(let i = 0; i < stream.length; i++) { 
  5.     action = stream[i]; 
  6.     state = reducer(state, action); 
  7.   } 
  8.   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)

  1. const redux = (reducer, initialState, stream) => { 
  2.   let currentState = initialState; 
  3.   let action
  4.  
  5.   const dispatch = action => { 
  6.     currentState = reducer(currentState, action); 
  7.   }; 
  8.   const getState = () => currentState; 
  9.  
  10.   for(i = 0; i < stream.length; i++) { 
  11.     action = stream[i]; 
  12.     dispatch(action); 
  13.   } 
  14.   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)

  1. const redux = (reducer, initialState) => { 
  2.   let currentState = initialState; 
  3.  
  4.   const dispatch = action => { 
  5.     currentState = reducer(currentState, action); 
  6.   }; 
  7.   const getState = () => currentState; 
  8.  
  9.   return ({ 
  10.     dispatch, 
  11.     getState, 
  12.   }); 
  13. }; 
  14.  
  15. const store = redux(reducer, initialState); 
  16. const action = { type, payload }; 
  17. store.dispatch(action); 
  18. 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,偽代碼如下所示:

  1. const store = { 
  2.   private currentState: initialState, 
  3.   public dispatch: (action) => { currentState = reducer(currentState, action)}, 
  4.   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)】

戳這里,看該作者更多好文

責(zé)任編輯:武曉燕 來(lái)源: 51CTO專(zhuān)欄
相關(guān)推薦

2016-10-31 11:26:13

ReactRedux前端應(yīng)用

2018-01-12 14:57:06

React Nativ開(kāi)發(fā)錯(cuò)誤

2010-06-22 13:32:26

函數(shù)式編程JavaScript

2022-06-10 08:01:17

ReduxReact

2023-11-06 07:37:01

函數(shù)式插槽React

2010-08-03 08:54:07

JDK 7Lambda表達(dá)式函數(shù)式編程

2019-01-17 10:25:56

Python編程語(yǔ)言程序員

2010-03-11 17:46:29

Pythond類(lèi)

2013-09-09 09:41:34

2020-09-23 16:07:52

JavaScript函數(shù)柯里化

2012-09-21 09:21:44

函數(shù)式編程函數(shù)式語(yǔ)言編程

2020-02-06 19:12:36

Java函數(shù)式編程編程語(yǔ)言

2024-12-05 10:37:36

Java純函數(shù)final

2010-03-15 10:24:20

Python函數(shù)變量

2023-10-07 00:01:02

Java函數(shù)

2024-04-22 09:12:39

Redux開(kāi)源React

2020-10-23 09:26:57

React-Redux

2020-06-12 08:22:27

React ReduxReact開(kāi)發(fā)

2020-09-24 10:57:12

編程函數(shù)式前端

2011-03-08 15:47:32

函數(shù)式編程
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)