怎樣理順react,flux,redux這些概念的關係,開發中有必要使用它們嗎?


一、引子

這是關於一把玄鐵重劍,一本經書,和一套輕功步法的故事。

讓我們先從普通程序猿們的日常工作內容說起,

一般來說,程序猿們大部分時間關注的可能不是研發某個具體演算法,這是演算法工程師/數學家們擅長的東東。程序猿的工作主要是通過調用編程環境中現成的工具函數或介面來實現具體的應用功能,將各個底層介面或演算法模塊用代碼有秩序地拼裝聯接起來,實現酷炫好用的產品功能,如同組裝一件樂高玩具一樣。

也就是說程序猿的很多工作往往不是圍繞某個高大上的具體演算法(「我們不生產演算法,我們只是演算法的搬運工」),而是像代碼界的城管、或者清潔工一樣,關注怎樣組織文件結構,怎樣理清編程思路,怎樣命名變數,怎樣降低代碼耦合度,怎樣提高代碼的復用性和一致性,提高代碼的可讀性和健壯性,怎樣優化分工協作、減少溝通成本等等。不管是OOP、FP等編程思想,還是MVC等設計模式、或是各種編程語言下的應用開發框架,很多都是為了幫助程序猿完成這些臟活、累活兒。

具體到web應用開發而言,react以及他的好基友redux都是程序猿們出色的好幫手,因此讓眾多前端開發者一見傾心,俺也不例外。

和一般前端框架相比,react有兩個顯著特點:

  1. react的性能很好,可以滿足實際生產環境下的絕大部分性能需求。
  2. react從使用的角度來說非常輕量級,因此很容易和其他順手常用的工具搭配使用,而沒有任何違和感。(如果你發現react可以和backbone、angular等框架很輕鬆地放到一起使用,請保持高冷,無需驚訝~)

事實上,react和redux從使用的角度來說,是如此輕量舒適,以至於我們可以不把它們當作「開發框架」,而是一種編程模式,或是編程的「腳手架」,用起來非常「小清新」。這一點和angularjs這類「重口味」框架有很大區別(我不推薦使用angularjs,當然每個人口味不同,最好自己上手體驗再做取捨)。其實本人接觸react比較晚,但一試用就有種血槽猛漲的感覺,強烈建議還沒上手react的前端程序猿們試用一下!

其實,關於react的具體開發實踐,知乎上已有很多優秀的文章,例如[react 有哪些最佳實踐? - 前端開發],不再贅述。這裡只想分享一下我在學習react過程中的一點體會,捋一下react以及flux背後思想的來龍去脈,從而能夠讓大家更加自然地接受react開發模式

二、正文

我們首先舉兩個栗子:

(一)江湖舊事1

在web1.0的純網頁時代,前端開發其實是比較happy的,這是因為網頁上幾乎不需要什麼交互,前端開發者基本上只需要根據後台提供的數據將網頁內容排版呈現出來即可。用戶的交互行為一般僅限於填寫一個表單,然後把數據提交到伺服器,提交成功後,直接刷新整個頁面

我們以常見的todoList為例,當需要添加一條todo任務的時候,用web1.0的思路,典型的流程是這樣的:

通過這張圖,我們再仔細揣摩一下為什麼web1.0條件下前端開發是比較輕鬆的:這是因為所有和數據(大體上等價於state)相關的操作,都已經由伺服器完成了,前端開發只需要根據state來決定view(頁面)。也就是說,這個時候前端開發者的思維是一個從state到view的「單向流」。(當state變化時,只要簡單粗暴地整體刷新頁面就OK了。)然而,當頁面交互變得豐富細膩、內容變得龐大複雜之後,這種基於伺服器維護state,然後頁面整體刷新的web1.0方式存在兩個嚴重的缺陷:

  1. 反覆刷新頁面,尤其是內容複雜的頁面,對瀏覽器的渲染性能消耗很大。一個很小的局部交互動作,就會引起整個頁面的刷新,顯然對於瀏覽器性能是非常大的浪費。同時,由於頁面刷新,導致不必要的等待和「閃屏」,這些對於有節操的產品汪或者程序猿都是難以容忍的。

  2. 由交互產生的很多細膩的前端數據,其實也很難交給後台處理,因為這是我們無處安放的臨時狀態。例如一個菜單是收起還是打開,一個面板是隱藏還是彈出,如果前端不去記錄這些view對應的狀態,那麼後台就要記錄這些狀態,否則頁面刷新後,這些狀態信息就會丟失。即使我們不在乎頁面和伺服器之間通信的時間浪費,我們也很難想像有什麼理由要在伺服器上記錄這些只和view相關的臨時狀態,畢竟這些狀態不對應任何後台業務數據。

正是由於這兩個顯著缺陷,才導致ajax技術的出現。自從有了ajax通信和局部頁面更新的機制以後,媽媽再也不用擔心我們頁面整體刷新導致的用戶體驗問題了!於是前端開發大踏步地進入web2.0時代,大量交互細膩,內容豐富的SPA(single page application)也應運而生。但與此同時,前端開發的工作也從此變得苦逼起來。。。因為現在根據後台數據排版生成頁面,現在只是最基本的工作。當用戶進行某種交互操作引起頁面狀態變化之後,為了避免頁面整體刷新,我們需要小心翼翼地將各個相關的頁面局部元素揀選出來,做適當修改,再放回原處,動作一般不會太優雅。(腦補一下《加勒比海盜》中,把眼球拿出來擦一擦又塞回去的那個海盜。。)

當頁面邏輯簡單,交互行為較少時,這種局部修改也許很容易搞定;然而,一旦頁面中各個元素的關係變得複雜,各種交互操作耦合起來之後,這種局部修改就會消耗大量腦細胞,並且我們需要同時對用戶操作和伺服器反饋做出響應,並確保頁面狀態和伺服器狀態的一致性,於是我們就很容易變得左支右絀、顧此失彼。相信不少前端工程師都有過類似的體驗。

總之,從上面的例子我們得出兩個經驗:

  1. 根據確定的交互狀態(state),一股腦兒決定頁面的呈現(view),這種「單向流」的開發狀態對程序員來說是思維清晰、比較輕鬆的;一旦我們需要不斷手動更新view,並且改變state和view的代碼還糾纏在一起,我們的內心往往是崩潰的。

  2. 為了讓前端開發不感到崩潰,就把所有state交給後台來維護,簡單粗暴地通過重新載入頁面來實現view的更新是不靠譜的(後台的內心獨白:我招誰惹誰了啊!!),我們需要找到新的方法,來實現view的自動更新。

(二)江湖舊事2

其實,通過改變state,來讓view自動更新,這個想法一點也不新鮮。css中各種基於className的樣式聲明,就是典型代表(改變div的className相當於改變它對應的state, 尤其是css3引入transition和animation之後,這種用css來偷懶的做法越來越常見),我們再舉個例子,比如說我們要做一個菜單:

生手碰到這個問題,也許會這樣做:

當用戶點擊按鈕的時候,先改變菜單名的色值,再改變按鈕的背景圖(由向下箭頭,改成向上箭頭),最後改變下拉列表的顯示狀態,有必要的話,還需要做個列表下拉的動畫。當用戶再次點擊按鈕的時候,把所有操作倒著來一遍,恢復菜單收起的狀態,ok?

然而,對於好逸惡勞的前端老司機們來說,一般會這樣做:對整個菜單的容器定義兩個className,比如一個是"menu-close",一個是「menu-open」。在menu-close的狀態下,菜單名的色值為黑色,按鈕背景圖為向下箭頭,下拉列表是隱藏狀態;在menu-open的狀態下,菜單名的色值為白色,按鈕背景圖是向上箭頭,下拉列表是顯示狀態;需要的話,再用css3 transition給下拉列表加個動畫過渡效果。當用戶點擊按鈕的時候,只需要改變整個菜單容器的className即可。所有的交互效果都用css「平鋪」的方式聲明出來,這樣做不僅節省了多個手工步驟,而且更改整個容器的className是個單點操作,因此也方便維護和修改。這也是bootstrap框架里慣用的一招,非常好使。

通過這個例子,我們看到「改變state,讓view自動更新」的開發思想在純粹的前端領域也由來已久。它可以讓開發者思維更加清晰,代碼更好維護,幸福指數飆升!

然而用css className來實現state功能,其應用範圍是非常有限的。原因很簡單,css只能改變DOM元素的樣式,卻不能改變網頁中DOM tree本身的結構(例如,例子1中todoList的增刪操作就涉及li元素的append或者remove),更沒有辦法直接和具體的業務數據相關聯。於是,knockoutjs、angularjs等前端框架紛紛登場,這些框架可以系統地實現view 和 state(一般在這些框架里稱為ViewModel)的相互綁定,從而使代碼更有秩序,幫前端開發省去不少麻煩。即便這些框架約定很多,有些束手束腳的感覺,但如果沒有另一件大殺器重出江湖,程序猿們也可以將就過了,奈何既生瑜,何生亮。。這是後話。

(三)重劍無鋒

好,拋開knockout、angular這些框架,現在如果讓我們自己設計一套根據states(包括後台業務數據前端臨時數據,例如表單input值、某個面板的顯隱狀態等等)自動更新頁面的機制,我們該怎麼辦呢?

對於每一種特定場景,我們也許有很多種代碼方式來根據state自動更新頁面的某個局部view。但是,如果頁面交互足夠複雜,以至於我們需要在頁面的很多地方不斷修修補補,並且這些「補丁」對應的state可能還彼此重疊,或者我們希望能一勞永逸地解決所有view自動更新的問題,並且還不引入更多繁瑣的約定,似乎除了刷新整個頁面沒有更好的辦法(這有點像逐幀動畫的原理,我們一般不會真的根據物體運動軌跡一點一點修改畫面,形成運動效果,而是直接一幀一幀*重繪整個畫布,形成動畫效果!)

但前面我們說過,像web1.0的做法一樣,重繪整個頁面對瀏覽器的性能損耗是很嚴重的,用戶體驗也很糟糕。。怎麼辦?怎麼辦?!我腦補著facebook的某個程序員在一個月黑風高的晚上坐在公司電腦前,抿了一口濃濃的咖啡,突然靈光一現,伴著屏幕上忽明忽暗的幽幽藍光,在文本編輯器里寫下這麼一行文字:可不可以把瀏覽器里的DOM tree克隆一份完整的鏡像到內存,也就是所謂的「virtual DOM」,當頁面的state發生變化以後,根據最新的state重新生成一份virtual DOM(相當於在內存里「刷新」整個頁面),將它和之前的virtual DOM做比對(diff),然後在瀏覽器里只渲染被改變的那部分內容,這樣瀏覽器的性能損耗和用戶體驗不就都不成問題了嗎?而我們知道在絕大部分網頁應用中js引擎的性能和內存完全沒有被充分利用,我們正好可以火力全開,利用js的這部分性能紅利,實現內存中virtual DOM的diff工作,完美!

於是React橫空出世。

話雖簡單,不過單單是在內存中模擬整個DOM tree,這個工作想想就覺得頭大,所以不得不佩服facebook的那些前端大神們!表面上react花了很大氣力卻只做了view層跟virtual DOM相關的工作,但所謂「大巧不工、重劍無鋒」,實際上facebook祭出的這件大殺器讓「改變state,view自動更新」這種直觀樸素的想法有了堅實的基礎,「state-view」的開發模式從此一馬平川!正因為如此,伴隨著react的崛起,類似於redux這些專註於管理state的輕量級框架也變得炙手可熱起來。

有了React這把重劍,前端開發們第一次感覺到似乎又回到了web1.0美好的田園時代!而react非常具有表達力的jsx語法和完善的模塊化結構,又讓我們覺得像生活在酷炫的未來時代!這是我們的下一話題。( 此處應有《Back to The Future》的電影配樂)

(四)庖丁解牛(view的模塊化)

view的組件化和模塊化非常有利於分工協作、代碼的積累復用以及單元測試。這對於提高團隊開發的效率無疑具有非常重要的意義,這也是react廣受青睞的重要原因之一,這一點就不再贅述。這裡想換個角度,聊一下react的模塊化機制,對於開發者個體來說有什麼好處?

前面我們提到,由於React的「state-view」模式可以讓開發者的大腦得到一種「單向流」的舒適體驗。那為什麼單向流的思維狀態更加舒適呢?

這是因為在**單向流**狀態下,要解決的問題如同一個函數映射,已知什麼(比如state)是固定不變的,要得到什麼(比如view)是定義明確,而人的思維非常習慣於這種定義明確的、沒有「分叉」和「環路」的函數式問題

也就是說,讓人抓狂崩潰的往往是那些包含「分叉」或「環路」的非函數式問題,這個時候大腦不得不思前想後,謀劃全局,進入一種「多線程」工作狀態,而「單線程」作業對於大腦來說一般才是更加輕鬆高效。所謂「單線程」,就是每時每刻只專註於一個問題 —— one at a time! React的"state-view"模式幫助我們在開發view的時候,只需要專註於view(即關注頁面布局樣式。view上的交互行為怎樣對state產生反饋作用,我們稍後再來討論),而React簡便的「模塊化」機制(即只需要寫很少量的boilerplate代碼,就可以定義或引用一個新模塊),讓我們可以根據需要,將整個頁面的布局樣式工作進一步拆分成各個小模塊(view component),或者將各個小模塊組裝成大模塊,從而進一步深入貫徹「one at a time」的原則,給大腦減負,因此這時程序猿很容易進入一種舒適高效的狀態(心理學中甚至有個「心流」的概念用來描述這種狀態)。決定頁面呈現的state可以通過模塊屬性(props)從父模塊傳遞到子模塊。這種"樹狀"分流機制,有點像植物將養分(state)從根部不斷運輸到細枝末葉的過程,如圖所示:

簡便的「模塊化」機制客觀上鼓勵了迅速上手、邊走邊看的敏捷開發方式。也就是說,前端開發者在做頁面布局樣式工作的時候,可以根據自己的經驗或視角,隨意挑選一塊比較「容易上手」的區域作為一個模塊的出發點,迅速開工,而無需事先瞻前顧後、謀劃全局。當挑選的區域中,有某一塊內容需要仔細打磨、細化功能的時候,我們可以迅速將這塊內容拆分為一個子模塊,然後專註於這個子模塊的開發,當若干模塊開發完畢後,我們也可以將各個模塊組裝成大模塊,最終形成整個頁面。這種開發體驗,有點像拼樂高或者創作雕塑:比如說我們要做一個人物雕塑群,我們可以先從單個人物開始。當我們在做單個人物的時候,我們可以先粗線條地捏出整體形態,在需要的時候,我們可以換用更稱手的工具來雕琢細節,比如人物的五官表情或者衣服的褶皺,如果一上來我們需要謀劃雕塑群中每一個人物的每一個表情細節才能開工,顯然這個任務是很難完成的。

好,到目前為止,我們看到react已經幾乎完美地幫我們理順了從state到view的開發流程。但是前面的討論過程中,似乎還有一朵「小烏雲」沒清理,那就是怎樣實現從view到state的反饋流程。也就是說,用戶在view上的交互行為(比如點擊提交按鈕等)應當引起state改變的時候,這個流程該怎麼處理?這是我們要聊的下一話題。

(五)易筋經(flux思想)

a. 幾點說明

如果要用一門武林絕學比喻flux思想,我首先想到的是易筋經。此功雖無固定招式,但意會之後,就會有種打通全身經絡、氣血通暢的感覺,並且可以將前端開發中的其他武功串聯起來,運用自如 : )

關於flux首先有幾點需要說明:

  1. flux與react沒有直接的關係,二者是完全獨立的概念。

  2. flux不是一個js庫,而是一種前端代碼的組織思想,比如說redux庫可以認為是一種flux思想的實現。

  3. flux的核心思想和代碼實現雖然很簡單(基本上就是一個event dispatcher而已),但在「state-view」開發模式中,卻是非常重要的一個環節,所以facebook給這個思想特意起了這麼一個「高大上」的名字。

b. MVC模式

在講flux之前,我們不得不首先提一下大名鼎鼎的MVC開發模式。

所謂MVC開發模式, 主要講的是在開發交互應用時,怎樣將不同功能的代碼拆分到不同文件或區塊,以便降低代碼的耦合度,提高代碼的可讀性和健壯性。簡單理解就是:要將 Model-View-Controller 這三部分代碼拆分到不同文件

對於伺服器端開發,Model指的是和處理業務數據相關的代碼,例如通過ORM實現資料庫的增刪改查等操作;View指的是和頁面組裝相關的代碼,主要是和各種模版引擎(例如Java Velocity、PHP Smarty、nodejs ejs等等)相關的代碼部分;Controller指的是和用戶交互行為相關的代碼,具體到網站後台應用,指的就是對各種http請求的handler,也就是根據不同**url路徑和http請求的參數**,將數據和模版綁定到一起,最終形成頁面呈現給用戶。

對於網站前端開發,在web1.0時代,由於js基本上只是個跑龍套的小角色,所以不需要什麼設計模式;但是隨著web應用功能變得越來越豐富、越來越複雜,js的地位也越來越靠近舞台中心。在目前情況下,如果沒有一定的設計模式作為指導,其實很難開發出真正大型複雜的html5應用,也很難實現分工協作和持續維護。

於是MVC模式被很自然地引入到前端開發領域。也許某種意義上,前端開發的整個MVC,僅僅對應於後台開發眼中的View部分;但其實,如今前端MVC思想的深入性和重要性,對於整個web應用來說,其實一點也不遜色於伺服器端的MVC。

具體而言,前端開發的Model相當於後台數據的鏡像或緩存池,它和伺服器端MVC中的Model概念一脈相承;View對應頁面的呈現,主要指的是和html、css相關的代碼,它和伺服器端MVC中的View概念也非常相近。

顯著的差別來自於controller:在後台應用中,用戶和伺服器之間的交互是通過http請求實現的,因此後台controller的表達形式是http請求的handler,並且和router(定義網站的url規則)緊密相關; 而前端應用中,用戶和網頁之間的交互主要是通過操作事件(例如點擊滑鼠、鍵盤輸入等)實現的,因此前端的controller這裡可以簡單理解為各種交互事件的handler。

當然, 前端controller的概念是個大雜燴,比如angularjs中的controller被定義為一個作用域($scope)的閉包, 參考AngularJS文檔,這個閉包可以和一段html模版綁定在一起,最終將數據渲染到模版中形成頁面。大約正是因為這種將數據和模版綁定的功能,非常類似於後台應用中的controller,因此很多框架包括angular將這種功能模塊稱為controller。為避免混淆,強調一下:後面我們用controller指代「凡是和交互事件handler相關的代碼單元」

backbonejs 是一個廣受歡迎的輕量級MVC前端框架,我們先來看一個 Backbone.js Todo Example,下面是其View組件的代碼片斷:

注意到這裡createOnEnter是對鍵盤輸入事件的響應,因此可視為一小段controller代碼(即controller是「內嵌」在View組件中的),並且這段代碼中對Todos (Model)進行了直接操作,即View直接對Model產生修改(這是個危險的信號!因為前面我們在討論「state-view單向流」的時候提到:一旦改變state和改變view的代碼糾纏在一起,程序猿的內心就比較容易崩潰...)。也就是說backbone框架的MVC模式可以用下圖表示:

我們再來看angularjs的todoList實例 (AngularJS Example:),可以發現其MVC模式(也有人稱angular的模式為MVVM,這麼細膩的區分感覺沒太大必要)也完全符合上面的圖示。

雖然這種View對Model直接修改的方式非常直截了當,適合小型web應用,然而一但web應用中存在多個Model,多個View,那麼Model和View之間的決定關係就可能變得混亂,難以駕馭,如下圖所示:

也就是說類似於上述代碼片斷中常見的MVC模式,阻礙了Model和View的組件化拆分(只有Model和View個數較少的時候,依賴關係才比較清晰)。因此,facebook團隊總結說:MVC模式難以scale up! [參考鏈接](Facebook: MVC Does Not Scale, Use Flux Instead [Updated])

怎麼破??

c. Flux模式

前面我們提到「單向流」的思維狀態可以讓大腦更加輕鬆駕馭,本質上而言,這也是為什麼上面這種雜亂的雙向圖示讓我們感到無所適從的原因。我們注意到:之所以圖示中 Model-View (MVC中的Model大體上可以看作是前面提到的State)的「單向流」被破壞,是由於修改Model的Controller代碼像一把黃豆一樣散落在了各個View組件的內部,如果可以用某種方式把這些散落的代碼單獨收攏到一起,是不是就讓這可以讓這張圖示恢復秩序呢?好,我們順著這個思路想下去。

現在我們又可以從伺服器端的MVC模式中獲得靈感了!因為我們注意到,伺服器端的controller通常也需要對很多Model產生修改,但在代碼結構中卻集中在一起,沒有散落一地。原因很簡單,由於server和client是遠程通信的關係,因此為了盡量減少通信耦合,client每個操作的全部信息都以http請求的形式被概括成了精簡的「作用量」(action)。請求的url路徑約定了用戶的操作意圖(當然RESTful概念中,請求的method也可以反映操作意圖),request參數表徵了該「意圖」的具體內容。正是基於這個action的抽象,client端的交互操作才可以被集中轉移到server端的controller中做統一響應。

對比之下,我們立刻發現上述代碼片斷中前端MVC模式的「痛點」所在:不是MVC模式錯了,而是我們壓根缺少了一個和用戶交互行為有關的action抽象!因此,對model的具體操作才沒法從各個view組件中被剝離出來,放到一處。

參考http請求,我們將要定義的action,需要一個typeName用來表示對model操作的意圖(類似於http請求的url路徑),還可能需要其他欄位,用來描述怎樣具體操作model(類似於http請求的參數)。

也就是說,當用戶在view上的交互行為(例如點擊提交按鈕)應當引起Model發生變化時,我們不直接修改model,而是簡單地dispatch一個action(其實跟常見的event機制沒有什麼區別)以表達修改model的意圖,這些action將被集中轉移給數據端(models),然後數據端會根據這些action做出需要的自我更新。同時,我們考慮到react中view組件的樹狀分流結構,所以有如下圖所示:

圖中A表示Action,V表示View組件,Models部分的結構會進一步討論。稍微總結一下:從代碼層面而言,flux無非就是一個常見的event dispatcher,其目的是要將以往MVC中各個View組件內的controller代碼片斷提取出來放到更加恰當的地方進行集中化管理,並從開發體驗上實現了舒適清爽、容易駕馭的「單向流」模式。 所以我覺得,Flux與其說是對前端MVC模式的顛覆,倒不如說是對前端MVC思想的補充和優化。

但為了區分於以往的MVC模式,並向facebook的貢獻表達敬意,後面我們將把這種優化後的 Model-View-Controller 開發模式在React背景下正式稱為Flux模式

好,易筋經Flux練到這裡,打完收工。到目前為止,我們看到React的獨孤九劍可以通過View Component把頁面呈現進行「原子化」拆分(即上圖中蘭色區域的樹狀分流結構);Flux打通了State-View的任督二脈(綠色區域),並通過action抽象把用戶交互行為進行了「原子化」拆分;那麼聯繫上面的圖示,我們自然要問數據端(紫色區域)的處理,可否同樣被「原子化」拆分?這是我們要聊的下一門武林絕學。

(六)凌波微步(數據端的「原子化」)

redux 登場

凌波微步指的是redux中的reducer機制,可以用來將state端的數據處理過程作「原子化」拆分。redux是來自函數式編程(Functional Programming)的一朵奇葩,據說很有背景([參考鏈接](Prior Art | Redux) )。本人還沒有深究過,但一接觸redux,就立刻被其reducer機制的輕盈小巧驚艷到(redux庫本身也只有幾kb,有必要的化,自己重寫也不是難事),因此稱其為「凌波微步」。

reducer,從代碼上說,其實就是一個函數,具有如下形式:

(previousState, action) =&> newState

即,reducer作為一個函數,可以根據web應用之前的狀態(previousState)和交互行為(通過flux中提到的action來表徵),決定web應用的下一狀態(newState),從而實現state端的數據更新處理。這個函數行為和大名鼎鼎的「Map-Reduce」概念中的Reduce操作非常類似,因而稱這個函數為「Reducer」。

"shut up and show me the code"

ok,我們還是以todoList應用為例, 此處有[完整代碼](Example: Todo List)。這裡不打算詳細講解Redux的具體使用,而只想通過一個Redux對state數據進行操作的代碼片斷,管窺一下reducer機制對數據進行拆分和組裝的簡潔過程。代碼片斷如下:

其中的todos是和任務列表數據相關的reducer,todo是和單條任務數據有關的reducer。注意:在todos的函數體內調用了todo,並將action作為參數原樣傳遞給了todo,這種乾淨利落地通過函數調用將action由 「parent reducer」 傳遞給 「child reducer」,是redux實現數據處理拆分的普遍方式。回味一下,我們應該可以體會到,這種數據處理「原子化」拆分的方式和react中view組件的拆分有異曲同工之妙,二者都會形成一種「樹狀」分流結構(在react的view hierarchy中,數據通過props的直接賦值實現單向流;在redux的reducer hierarchy中,數據通過action的函數傳參實現單向流)。

visibilityFilter是和列表顯示狀態相關的另一個reducer;combineReducers將visibilityFilter和todos合併為整個應用的reducer,也就是todoApp。這個過程,從感覺上也和react中view組件的合併過程非常相像。

createStore是一個工廠函數。通過它,todoApp(相當於一個數據處理的引擎)被裝配到整個應用的state容器,也就是store中。可以通過store的getState方法獲取整個應用的state;同時,store也是一個event dispatcher,可以通過其dispatch和subscribe方法,分別實現觸發action事件和註冊對action事件的響應函數。總言之,從概念上來說 Redux = Reducer + Flux

三、總結

全體亮相

好,現在React開發模式中的幾個核心概念已經全部出場亮相。我們俯瞰一下整個開發流程:首先,react框架為我們理順了 store --&> view 的「單向」工作流(store是state的容器);然後,redux框架為我們理順了 view --&> store 的**「單向」**工作流。並且,react和redux都以組件化的形式可以將各自負責的功能進行靈活地組裝或拆分,最大程度上確保我們「一次只需要專註於一個局部問題」。具體來說,分為以下步驟:

  1. 單例store的數據在react中可以通過view組件的屬性(props)不斷由父模塊**「單向」**傳遞給子模塊,形成一個樹狀分流結構。如果我們把redux比作整個應用的「心肺」 (redux的flux功能像心臟,reducer功能像肺部毛細血管),那麼這個過程可以比作心臟(store)將氧分子(數據)通過動脈毛細血管(props)送到各個器官組織(view組件)

  2. 末端的view組件,又可以通過flux機制,將攜帶交互意圖信息的action反饋給store。這個過程有點像將攜帶代謝產物的「紅細胞」(action)通過靜脈毛細血管又泵回心臟(store)

  3. action流回到store以後,action以參數的形式又被分流到各個具體的reducer組件中,這些reducer同樣構成一個樹狀的hierarchy。這個過程像靜脈血中的紅細胞(action)被運輸到肺部毛細血管(reducer組件)

  4. 接收到action後,各個child reducer以返回值的形式,將最新的state返回給parent reducer,最終確保整個單例store的所有數據是最新的。這個過程可以比作肺部毛細血管的血液充氧後,又被重新泵回了心臟

  5. 回到步驟1

用圖示的方式來表達,即,

圖中A表示Action,V表示View組件,R表示Reducer。為了確保我們比較容易理解程序的全局行為,或者說提高程序行為的確定性(predictable),我們一般期望具有類似職能的代碼片斷被「平鋪」著擺放在一。因此圖示中相同顏色區域的代碼通常會被放到同一個文件夾/文件中。另外,同樣出於提高程序的確定性,redux所遵循的函數式編程鼓勵我們使用pure function和immutable。(函數式編程是另一個漫長的故事,這裡就不再展開)

要你命三千

當然實際的react-redux開發步驟中也有不少變通,例如為了能在適當條件下節省些參數傳遞的代碼,redux中提供了provider機制,它像《星際迷航》中的瞬間傳送機一樣,可以繞過view組件的hierarchy把state一步到位地傳遞到末端view組件;再比如redux雖然鼓勵使用pure function(即不含side effect的函數), 但我們一般還是會通過side effect實現store和伺服器端的通信。這些變通,覺得是好事。畢竟再嚴格的框架,也經不起胡亂使用;再完美的框架,也很難說能包治百病,倒不如提供一些台階,讓程序猿們去自由發揮吧。

除了React、Reducer、Flux這三駕馬車的主線情節外,react開發模式的周邊生態也有很多振奮人心的新鮮事物,例如:擁抱函數式編程和模塊化的es6語法,日趨成熟的模塊資源管理工具npm,自動化編程及打包神器webpack, 讓初始化性能和SEO不再成為問題的server-side rendering方案,將觸角伸到原生開發領域的React-Native系列項目等等,這些零零總總加起來,不由得讓人想起《國產凌凌漆》中集各種武器於一身的終極武器 「要你命三千」!

一把玄鐵重劍,一本易筋經,一套輕功步法,以及「要你命三千」!實乃居家旅行、應用開發必備良藥!


一開始有了react,觸發事件、處理事件、改變狀態等等全部都是在一個組件里寫的,這不清真。

然後facebook出了flux,拆分為三個主要部分Dispatcher調度 、存儲Store和視圖View(React 組件)

"Flux是Facebook用來構建客戶端Web應用的應用架構。它利用單向數據流的方式來組合React中的視圖組件。它更像一個模式而不是一個正式的框架,開發者不需要太多的新代碼就可以快速的上手Flux。"

Redux是受flux的啟發,也採用單向數據流。在Redux中有一個單一的store對象,包含整個應用程序的state。普通react組件為components,redux容器型組件為Container,他們通過react-redux中的connect模塊連接。觸發一個事件,則dispatch一個action。然後到reducer,由reducer來更新state,最終反應到頁面上。


react是view engine,渲染出HTML頁面。

flux是Facebook為了react引進的一種模式,單向的數據流控制。是一種定義…

redux是flux的實現,可以理解flux是interface,redux是具體實現的class…


圖源:GitHub - kenberkeley/redux-simple-tutorial: Redux 莞式教程。本教程深入淺出,配套入門、進階源碼解讀以及文檔注釋豐滿的 Demo 等一條龍服務


寫前端的時候需要考慮的最基本的問題無非就是兩個:一是根據數據渲染頁面,二是根據頁面變動更新數據。在數據和界面頻繁變動、邏輯複雜的時候,處理著兩個問題就很不容易,需要框架。

react在這時候幫助做的事情,是形成單向數據流。這樣給出初始頁面和數據,當頁面或者數據變動時,reac能推導出新的狀態,複雜的計算交給自己的虛擬dom,然後重渲染。

但是即便做到這樣,依然有很多處理起來很複雜的地方,比如變動的數據可能來自伺服器,可能來自客戶端,頁面變動也可能是點擊、可能是下拉,還有各種路由變化等等。

flux 和 redux幫助做的事,就是為了保證每一個變動都能映射到確切的狀態,在尤其是邏輯非常龐雜的時候維護和控制單向數據流,相當於一個數據狀態容器。

至於redux和flux的關係,樓上有回答了差別在具體實現上,比如redux把state 和 reducer 做了分離,也沒有dispatcher,但理解起來本質做的事情是基本一樣的。

還有,flux 和 redux 並不局限於react,可以應用於其他前端框架,但是缺少了flux或者redux的react是不夠完善的。


自己在工作過程中做了一個總結,用很直觀的方式解釋下redux和react的關係,適用於真正開發當中。Flux在此就不涉及了,Flux本身是一套單項數據流的設計框架,redux是其的一種具體實現罷了。redux和react總一起出現是因為如果單單使用react,它僅僅是個view framework,不足以提供足夠的前端管理和使用功能。而redux的引入就好像react+ MC(model controller) 一樣,賦予了react完整的生態系統。當然redux不是基於MVC的。簡單說,redux + react換了個更直觀的法子實現了MVC能提供的數據管理功能。要理解下圖,最核心要理解幾點:

1. React本身是基於component的View

2. Redux是flux的一個實現,其核心有一個中心store用於管理全局state以及處理操作

3. redux和react本身並沒有任何關係。redux也可以用在MVC框架上,flux的理念可以用在任何網頁或移動前端上。比如iOS swift也有ReSwift的框架,在iOS的MVC之上實現了單向數據流。

4. redux和react雖然本身沒有關係,但是他們各自為戰都戰鬥力不足,合在一起確是能量值爆表,所以大部分時候,兩個東西會通過一個叫做react-redux的庫連接起來。這個庫最核心的一個fucntion叫做connect。

5. redux的核心,是action-store-reducer,action用於表示一個操作,store用於存儲狀態,reducer用於接收action並更新store中的狀態。

具體分析上圖:

我們從一個簡單的例子入手,比如,你有一個頁面簡單的呈現一下從後端得到的所有商品數據列表。首先,你load這個頁面。react有componentDidMount callback,可以直接調用需要的函數。在此,我們調用一個函數用來從後端獲取所有的商品。所以,

1. 在上圖中的第1步,react component2 發起了一個function call.

2. 第二步,這個function的內部實現,是dispatch一個action。action是redux的核心概念,用來代表我需要對store state做一些操作。這個action,比如我們叫他"GET_PRODUCTS_INDEX"被store.dispatch發到了store。

3. 第三步,store收到這個「GET_PRODUCTS_INDEX」 action,立馬把這個action發布給了訂閱的reducer,reducer是可以chain在一起的。reducer內部會去接收這個action。同時,多個reducer都可以同時接收這個action。

4. 第四步,假設只有reducer1想要接收這個action,他讀取action所攜帶的payload(這裡沒有payload), 然後更新store當中相應的state。一般對於backend call,都會有一個表示正在call的狀態,假設為isCalling。在此就需要把isCalling set成true。

5. state預先已經和react component的props做了綁定,就是通過react-redux庫裡面的定義mapStatesToProps實現的。所以store state一旦被更新,相應的props也就被更新。而props更新,會導致相關的UI做出re-render,導致UI變化。在這裡,因為isCalling變成了TRUE,我們可以顯示出一個loading的icon,代表正在載入數據。

這就是一個完整的redux和react合作的數據流的閉環。當然有人要問了,你這個GET_PRODUCTS_INDEX action怎麼最終也沒有拿到後端的數據啊?就僅僅是讓loading icon出現嗎?出乎你的意料,確實是的。從嚴格意義上講的一個redux 數據流,確實已經完了。那麼我們到底是需要在什麼地方真正的去後端取數據呢?實現這個需要一些別的輔助。比如,可以用"rxjs/Rx"做出一個EPIC的概念來。其核心就是,除了reducer可以監聽action之外,這個epic也可以監聽action,一旦發現GET_PRODUCTS_INDEX action 被dispatch了,它立馬用ajax的形式向後端發起請求。一旦請求返回,他用store dispatch一個新的action,比如叫GET_PRODUCTS_INDEX_SUCCESS。這個action攜帶payload,而這個payload就是返回的商品數據。

嘿熟悉嗎,沒錯,至此,新的一個redux數據流又從發起一個action dispatch開始了!重新走上面的1-5步就可以把相應的數據都給顯示出來啦。


小弟不才 有一點搞不太懂 redux 包括 vuex這些首先我們要搞清楚 它們是解決什麼問題 它們無非就是解決 一些不是包含或者上下級關係組件之間數據通訊問題 我這個回答沒錯吧 。。

那為什麼搞得這麼玄乎呢 直接自定義一個事件管理的東西 來派發和接受事件然後改變數據不就得了嗎 幹嘛搞得那麼複雜 繞來繞去。寫比如寫store 、reducer不蛋疼嗎?

這是我前幾天在簡書寫的一篇RN日誌 組件之間的通訊 我就是用自定義事件的方式來解決組件通訊的問題

歡迎大神們指點指點 告訴我一下 明明大家可以向我這樣寫個事件管理 就搞定的事情 為什麼還非要去寫那麼繞的redux呢 ?


就算你不用這幾個 在你的代碼裡面使用他們的理念也是可以的

把UI想像成一個state machine 你只是往裡面灌數據 UI自己演化


推薦閱讀:

Web 前端開發面臨的挑戰主要有哪些?
2016該如何學習nodejs?
請問學習前端最有效的辦法是什麼?
前端工程師都有用哪些比較靠譜的小工具?
為什麼 TypeScript 成功了,更先進的 ActionScript 卻失敗了?

TAG:前端開發 | 應用開發 |