Redux是一個可預測的狀態容器,不但融合了函數式編程思想,還嚴格遵循了單向數據流的理念。Redux繼承了Flux的架構思想,并在此基礎上進行了精簡、優化和擴展,力求用最少的API完成最主要的功能,它的核心代碼短小而精悍,壓縮后只有幾KB。Redux約定了一系列的規范,并且標準化了狀態(即數據)的更新步驟,從而讓不斷變化、快速增長的大型前端應用中的狀態有跡可循,既利于問題的重現,也便于新需求的整合。注意,Redux是一個獨立的庫,可與React、Ember或jQuery等其它庫搭配使用。
  在Redux中,狀態是不能直接被修改的,而是通過Action、Reducer和Store三部分協作完成的。具體的運作流程可簡單的概括為三步,首先由Action說明要執行的動作,然后讓Reducer設計狀態的運算邏輯,最后通過Store將Action和Reducer關聯并觸發狀態的更新,下面用代碼演示這個流程。
~~~js
function caculate(previousState = {digit: 0}, action) { //Reducer
let state = Object.assign({}, previousState);
switch (action.type) {
case "ADD":
state.digit += 1;
break;
case "MINUS":
state.digit -= 1;
}
return state;
}
let store = createStore(caculate); //Store
let action = { type: "ADD" }; //Action
store.dispatch(action); //觸發更新
store.getState(); //讀取狀態
~~~
  通過上面的代碼可知,Action是一個普通的JavaScript對象,Reducer是一個純函數,Store是一個通過createStore()函數得到的對象,如果要觸發狀態的更新,那么需要調用它的dispatch()方法。先對Redux有個初步的感性認識,然后在接下來的章節中,將圍繞這段代碼展開具體的分析。
## 一、三大原則
  只有遵守Redux所設計的三大原則,才能讓狀態變得可預測。
  (1)單一數據源(Single source of truth)。
  前端應用中的所有狀態會組成一個樹形的JavaScript對象,被保存到一個Store中。這樣不但能避免數據冗余,還易于調試,并且便于監控任意時刻的狀態,從而減少出錯概率。不僅如此,過去難以達成的功能(例如即時保存、撤銷重做等),現在實現起來也變得易如反掌了。在應用的任意位置,可通過Store的getState()方法讀取到當前的狀態。
  (2)保持狀態只讀(State is read-only)。
  若要改變Redux中的狀態,得先派發一個Action對象,然后再由Reducer函數創建一個新的狀態對象返回給Redux,以此保證狀態的只讀,從而讓狀態管理能夠井然有序的進行。
  (3)狀態的改變由純函數完成(Changes are made with pure functions)。
  這里所說的純函數是指Reducer,它沒有副作用(即輸出可預測),其功能就是接收Action并處理狀態的變更,通過Reducer函數使得歷史狀態變得可追蹤。
## 二、主要組成
  Redux主要由三部分組成:Action、Reducer和Store,本節將會對它們依次進行講解。
**1)Action**
  由開發者定義的Action本質上就是一個普通的JavaScript對象,Redux約定該對象必須包含一個字符串類型的type屬性,其值是一個常量,用來描述動作意圖。Action的結構可自定義,盡量包含與狀態變更有關的信息,以下面遞增數值的Action對象為例,除了必需的type屬性之外,還額外附帶了一個表示增量的step屬性。
~~~js
{ type: "ADD", step: 1 }
~~~
  如果項目規模越來越大,那么可以考慮為Action加個唯一號標識或者分散到不同的文件中。
  通常會用Action創建函數(Action Creator)生成Action對象(即返回一個Action對象),因為函數有更好的可控性、移植性和可測試性,下面是一個簡易的Action創建函數。
~~~js
function add() {
return { type: "ADD", step: 1 };
}
~~~
**2)Reducer**
  Reducer函數對狀態只計算不存儲,開發者可根據當前業務對其進行自定義。此函數能接收2個參數:previousState和action,前者表示上一個狀態(即當前應用的狀態),后者是一個被派發的Action對象,函數體中的返回值是根據這兩個參數生成的一個處理過的新狀態。
  Redux在首次執行時,由于初始狀態為undefined,因此可以為previousState設置初始值,例如像下面這樣使用ES6默認參數的語法。
~~~js
function caculate(previousState = {digit: 0}, action) {
let state = Object.assign({}, previousState);
//省略更新邏輯
return state;
}
~~~
  在編寫Reducer函數時,有三點需要注意:
  (1)遵守純函數的規范,例如不修改參數、不執行有副作用的函數等。
  (2)在函數中可以先用Object.assign()創建一個狀態對象的副本,隨后就只修改這個新對象,注意,方法的第一個參數要像上面這樣傳一個空對象。
  (3)在發生異常情況(例如無法識別傳入的Action對象),返回原來的狀態。
  當業務變得復雜時,Reducer函數中處理狀態的邏輯也會隨之變得異常龐大。此時,就可以采用分而治之的設計思想,將其拆分成一個個小型的獨立子函數,而這些Reducer函數各自只負責維護一部分狀態。如果需要將它們合并成一個完整的Reducer函數,那么可以使用Redux提供的combineReducers()函數。該函數會接收一個由拆分的Reducer函數組成的對象,并且能將它們的結果合并成一個完整的狀態對象。下面是一個用法示例,先將之前的caculate()函數拆分成add()和minus()兩個函數,再作為參數傳給combineReducers()函數。
~~~js
function add(previousState, action) {
let state = Object.assign({}, previousState);
state.digit = "digit" in state ? (state.digit + 1) : 0;
return state;
}
function minus(previousState, action) {
let state = Object.assign({}, previousState);
state.number = "number" in state ? (state.number - 1) : 0;
return state;
}
let reducers = combineReducers({add, minus});
~~~
  combineReducers()會先執行一次這兩個函數,也就是說reducers()函數所要計算的初始狀態不再是undefined,而是下面這個對象。注意,{add, minus}用到了ES6新增的簡潔屬性語法。
~~~js
{ add: { digit: 0 }, minus: { number: 0 } }
~~~
**3)Store**
  Store為Action和Reducer架起了一座溝通的橋梁,它是Redux中的一個對象,發揮了容器的作用,保存著應用的狀態,包含4個方法:
  (1)getState():獲取當前狀態。
  (2)dispatch(action):派發一個Action對象,引起狀態的修改。
  (3)subscribe(listener):注冊狀態更新的監聽器,其返回值可以注銷該監聽器。
  (4)replaceReducer(nextReducer):更新Store中的Reducer函數,在實現Redux熱加載時可能會用到。
  在Redux應用中,只會包含一個Store,由createStore()函數創建,它的第一個參數是Reducer()函數,第二個參數是可選的初始狀態,如下代碼所示,為其傳入了開篇的caculate()函數和一個包含digit屬性的對象。
~~~js
let store = createStore(caculate, {digit: 1});
~~~
  caculate()函數會增加或減少狀態對象的digit屬性,其中增量或減量都是1。接下來為Store注冊一個監聽器(如下代碼所示),當狀態更新時,就會打印出最新的狀態;而在注銷監聽器(即調用unsubscribe()函數)后,控制臺就不會再有任何輸出。
~~~js
let unsubscribe = store.subscribe(() => //注冊監聽器
console.log(store.getState())
);
store.dispatch({ type: "ADD" }); //{digit: 2}
store.dispatch({ type: "ADD" }); //{digit: 3}
unsubscribe(); //注銷監聽器
store.dispatch({ type: "MINUS" }); //沒有輸出
~~~
## 三、綁定React
  雖然Redux和React可以單獨使用(即沒有直接關聯),但是將兩者搭配起來能發揮更大的作用。React應用的規模一旦上去,那么對狀態的維護就變得愈加棘手,而在引入Redux后就能規范狀態的變化,從而扭轉這種窘境。Redux官方提供了一個用于綁定React的庫:react-redux,它包含一個connect()函數和一個Provider組件,能很方便的將Redux的特性融合到React組件中。
**1)容器組件和展示組件**
  由于react-redux庫是基于容器組件和展示組件相分離的開發思想而設計的,因此在正式講解react-redux之前,需要先理清這兩類組件的概念。
  容器組件(Container Component),也叫智能組件(Smart Component),由react-redux庫生成,負責應用邏輯和源數據的處理,為展示組件傳遞必要的props,可與Redux配合使用,不僅能監聽Redux的狀態變化,還能向Redux派發Action。
  展示組件(Presentational Component),也叫木偶組件(Dumb Component),由開發者定義,負責渲染界面,接收從容器組件傳來的props,可通過props中的回調函數同步源數據的變更。
  容器組件和展示組件是根據職責劃分的,兩者可互相嵌套,并且它們內部都可以包含或省略狀態,一般容器組件是一個有狀態的類,而展示組件是一個無狀態的函數。
**2)connect()**
  react-redux提供了一個柯里化函數:connect(),它包含4個可選的參數(如下代碼所示),用于連接React組件與Redux的Store(即讓展示組件關聯Redux),生成一個容器組件。
~~~js
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
~~~
  在使用connect()時會有兩次函數執行,如下代碼所示,第一次是獲取要使用的保存在Store中的狀態,connect()函數的返回結果是一個函數;第二次是把一個展示組件Dumb傳到剛剛返回的函數中,繼而將該組件裝飾成一個容器組件Smart。
~~~js
const Smart = connect()(Dumb);
~~~
  接下來會著重講解函數的前兩個參數:mapStateToProps和mapDispatchToProps,另外兩個參數(mergeProps和options)可以參考官方文檔的說明。
**3)mapStateToProps**
  這是一個包含2個參數的函數(如下代碼所示),其作用是從Redux的Store中提取出所需的狀態并計算成展示組件的props。如果connect()函數省略這個參數,那么展示組件將無法監聽Store的變化。
~~~js
mapStateToProps(state, [ownProps])
~~~
  第一個state參數是Store中保存的狀態,第二個可選的ownProps參數是傳遞給容器組件的props對象。在一般情況下,mapStateToProps()函數會返回一個對象,但當需要控制渲染性能時,可以返回一個函數。下面是一個簡單的例子,還是沿用開篇的caculate()函數,Provider組件的功能將在后文中講解。
~~~js
let store = createStore(caculate);
function Btn(props) { //展示組件
return <button>{props.txt}</button>;
}
function mapStateToProps(state, ownProps) {
console.log(state); //{digit: 0}
console.log(ownProps); //{txt: "提交"}
return state;
}
let Smart = connect(mapStateToProps)(Btn); //生成容器組件
ReactDOM.render(
<Provider store={store}>
<Smart txt="提交" />
</Provider>,
document.getElementById("container")
);
~~~
  Btn是一個無狀態的展示組件,Store中保存的初始狀態不是undefined,容器組件Smart接收到了一個txt屬性,在mapStateToProps()函數中打印出了兩個參數的值。
  當Store中的狀態發生變化或組件接收到新的props時,mapStateToProps()函數就會被自動調用。
**4)mapDispatchToProps**
  它既可以是一個對象,也可以是一個函數,如下代碼所示。其作用是綁定Action創建函數與Store實例所提供的dispatch()方法,再將綁好的方法映射到展示組件的props中。
~~~js
function add() { //Action創建函數
return {type: "ADD"};
}
var mapDispatchToProps = { add }; //對象
var mapDispatchToProps = (dispatch, ownProps) => { //函數
return {add: bindActionCreators(add, dispatch)};
}
~~~
  當mapDispatchToProps是一個對象時,其包含的方法會作為Action創建函數,自動傳遞給Redux內置的bindActionCreators()方法,生成的新方法會合并到props中,屬性名沿用之前的方法名。
  當mapDispatchToProps是一個函數時,會包含2個參數,第一個dispatch參數就是Store實例的dispatch()方法;第二個ownProps參數的含義與mapStateToProps中的相同,并且也是可選的。函數的返回值是一個由方法組成的對象(會合并到props中),在方法中會派發一個Action對象,而利用bindActionCreators()方法就能簡化派發流程,其源碼如下所示。
~~~js
function bindActionCreator(actionCreator, dispatch) {
return function () {
return dispatch(actionCreator.apply(this, arguments));
};
}
~~~
  展示組件能通過讀取props的屬性來調用傳遞過來的方法,例如在Btn組件的點擊事件中執行props.add(),觸發狀態的更新,如下所示。
~~~js
function Btn(props) {
return <button onClick={props.add}>{props.txt}</button>;
}
~~~
  通過上面的分析可知,mapStateToProps負責展示組件的輸入,即將所需的應用狀態映射到props中;mapDispatchToProps負責展示組件的輸出,即將需要執行的更新操作映射到props中。
**5)Provider**
  react-redux提供了Provider組件,它能將Store保存在自己的Context(在[第9篇](https://www.cnblogs.com/strick/p/10668965.html)做過講解)中。如果要正確使用容器組件,那么得讓其成為Provider組件的后代,并且只有這樣才能接收到傳遞過來的Store。Provider組件常見的用法如下所示。
~~~html
<Provider store={store}>
<Smart />
</Provider>
~~~
  Provider組件位于頂層的位置,它會接收一個store屬性,屬性值就是createStore()函數的返回值,Smart是一個容器組件,被嵌套在Provider組件中。
*****
> 原文出處:
[博客園-React躬行記](https://www.cnblogs.com/strick/category/1455720.html)
[知乎專欄-React躬行記](https://zhuanlan.zhihu.com/pwreact)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎瀏覽。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020