淺談Flux架構及Redux實踐
Flux概述
Flux是Facebook用來構建用戶端的Web應用程序的體系架構,與其它形式化的框架相比,它更像是一個架構思想,用于管理和控制應用中數據的流向。這里應用中的數據指包括但不限于來自服務端的數據頁面中view的一些狀態(如一個面板是展開還是關閉),臨時存儲在本地需要持久化到服務端的數據等。
好了,說了這么多好像還是一臉懵逼,不慌,接下來看看展開式。
MVC
在講述Flux之前,我們看看之前傳統的MVC架構以及在前端中的一些問題繼而思考Flux帶來的改變。MVC(Model-View-Controller)***興起于后端,通過對應用程序復雜度的簡化使程序更加直觀和便于維護。后端程序MVC中View可以看為數據的呈現,Model為數據的模型,Controller作為程序的流程控制。現在假設有這樣的場景,用戶想查看自己的profile頁面,可能會有這樣的流程:在頁面上點擊profile按鈕,接下來就是一個HTTP請求(/profile?username=jiavan) => Controller接收到這一請求并獲得請求的內容username=jiavan然后告知Model需要jiavan的數據 => Model返回了jiavan的數據 => Controller得到數據返回新的視圖,看下流程:
現在前端中又有這樣的場景:切換Menu中的Item,當前選中的Item顏色不同于其它顏色并且底部顯示對應Item的內容。一般情況下我們會定義一個css class來作為當前選中Item的樣式。當用戶點擊Item_A為被點擊的元素新增高亮的class,其它兄弟元素移除該樣式,這里的事件響應函數就是Controller,我們會在這里處理樣式修改邏輯,以及更新Model的數據,然后新的數據及樣式重新渲染界面。這種VC<->M的形式在關系比較簡單的情況下是比較清晰容易控制的,但是復雜的頁面上這樣的模式可能會變得非?;靵y:
之所以變得混亂了,因為很多view都具備修改多個model的能力,這里的單個修改行為可以稱之為一個Action,一個Action的產生可能是用戶行為,或者一個Ajax請求需要渲染新界面。對比上面后端傳統MVC模式可以發現:
- 后端中Action作為一個URL請求,前端中可能是一個事件;
- 后端中Action處理被集中在Controller中,而前端中是分散的。
那么是不是可以把前端中修改狀態即state的行為(事件回調/Ajax)全部抽象成一種Action描述,然后交付到一處即Reducers來進行原子化處理,然后Reducer修改整個應用中唯一的一棵state tree即Store,***通過state->view的機制來重新渲染?
Flux數據流框架
上面提到的幾個概念已經對Flux有了初步的了解,下面進入正題。相信有了解Flux的都應該看過下面這張著名的數據流圖:
- Action可以看成是修改Store的行為抽象;
- Dispatcher管理著應用的數據流,可以看為Action到Store的分發器;
- Store管理著整個應用的狀態和邏輯,類似MVC中的Model。
所以Flux可以被看作傳統MVC的改進而非顛覆,當我***次看到Flux的時候其實是比較懵逼,但看到并使用了Redux后確實有一種非常驚艷的感覺。
Redux
按照Redux官方的描述Redux is a predictable state container for JavaScript apps.,其中predictable和state container體現了它的作用。那么如何來理解可預測化的呢?這里會有一些函數式編程方面的思想,在Redux中reducer函數是一個純函數,相同輸入一定會是一致的輸出,所以確定輸入的state那么reducer函數輸出的state一定是可以被預測的,因為它只會進行單純的計算,保證正確的輸出。狀態容器又是什么?說明Redux有一個專門管理state的地方,就是Store,并且一般情況下是唯一的,應用中所有state形成的一顆狀態樹就是Store。Redux由Flux演變而來,但受 Elm 的啟發,避開了 Flux 的復雜性,我們看看其數據流向:
不同于Flux架構,Redux中沒有dispatcher這個概念,并且Redux設想你永遠不會變動你的數據,你應該在reducer中返回新的對象來作為應用的新狀態。但是它們都可以用(state, action) => newState來表述其核心思想,所以Redux可以被看成是Flux思想的一種實現,但是在細節上會有一些差異。
重要概念
- 應用中的所有state都以一個object tree的形式存儲在一個單一的store中;
- 唯一能改store的方法是觸發action,action是動作行為的抽象;
- 為了描述action如何改變state樹,需要編寫reducer函數。
這里需要說明一點的是reducer函數,它應當是一個純函數,不應該有副作用,不應有API調用,Date.now()或者隨機獲取等不穩定的操作,應當保證相同的輸入reducer計算的結果應該是一致的輸出,它只會進行單純的計算。編寫reducer函數也是Redux中比較重要的一塊,它的形式如下:
- function testReducer(state, action) {
- switch (action.type) {
- case ACTION_TYPE:
- // calc...
- return newState;
- default: return state;
- }
- return newState;
- }
state是不可修改的,所以返回的新state應該是基于輸入state副本的修改,而不是直接修改state后的返回。
原則
1. 單一數據源,store
整個應用的state被存放在一棵Object tree中,并且這個Object tree只存在唯一一個store中;
2. state是只讀的
唯一能改變state的方法是觸發action,action是對已經發生了的事情的抽象描述,簡單的講,它把行為抽象成了一個對象。
比如,刪除一條記錄的action可以抽象的理解為:
- {
- type: 'DELETE_ITEM',
- index: 1,
- }
3. 使用純函數來實現state歸并操作,reducer
傳入待修改的state和一個告知reducer如何修改state的action,reducer將返回action規則對應下操作后的新的state。
reducer(state, action) => new state
數據流
嚴格的單向數據流是Redux設計的核心
Redux應用數據的生命周期遵循下面4個步驟:
- 調用store.dispatch(action), 可以在任何地方進行;
- Redux store調用傳入的reducer函數,并且將當前的state樹與action傳入。reducer是純函數,只用于計算下一個state,它應該是完全可被預測的,相同的輸入必定會有相同的輸出,不能有副作用的操作,如API的調用或者路由跳轉,這些應該都是在dispatch前產生;
- 根reducer將多個子reducer輸出合并成一個單一的state樹;
- Redux store保存了根reducer返回的完整的state樹。
新的state樹就是應用的下一個狀態,現在就可以根據新的state tree來渲染UI。
Redux實踐
我們通過一個非常簡單的計數器demo來梳理Redux的數據流。
0x00. 創建action
action其實就是一個普通的對象,只是對行為的抽象描述,這里我們可以把加上一個數描述為:
- {
- type: INCREMENT, //該動作的抽象描述
- number, // 該動作攜帶的數據
- }
更多的時候我們會通過一個action生成函數來得到一個action:
- function incrementCreator(number) {
- return {
- type: INCREMENT,
- number,
- };
- }
0x01. 創建reducer函數
reducer作為整個Redux中action的處理中樞,接收state與action并對此修改數據,返回應用的下一個狀態。
- function countReducer(state, action) {
- switch (action.type) {
- case INCREMENT:
- return Object.assign({}, {
- counter: state.counter + action.number,
- });
- case DECREMENT:
- return Object.assign({}, {
- counter: state.counter - action.number,
- });
- default: return state;
- }
- }
注意:上面我們已經提到多次,state是不可修改的,所以通過assign歸并我們對數據的操作,返回的是state副本修改后的對象,并非直接修改了輸入的state。
0x02. 創建唯一store
通過Redux中的createStore方法傳入reducer函數來創建整個應用的store。
- const store = createStore(countReducer);
0x03. 修改state
通過store的dispatch方法來發起一個action。
- store.dispatch(incrementCreator(5));
- store.dispatch(decrementCreator(4));
完整demo
- import { createStore } from 'redux';
- // actions
- const INCREMENT = 'INCREMENT';
- const DECREMENT = 'DECREMENT';
- // actionCreator,可以視為創建action的語法糖
- function incrementCreator(number) {
- return {
- type: INCREMENT,
- number,
- };
- }
- function decrementCreator(number) {
- return {
- type: DECREMENT,
- number,
- };
- }
- // 初始化state
- const initialState = {
- counter: 0,
- };
- // reducers函數,注意***一定要return state防止不能匹配到action的時候state丟失
- function countReducer(state = initialState, action) {
- switch (action.type) {
- case INCREMENT:
- return Object.assign({}, {
- counter: state.counter + action.number,
- });
- case DECREMENT:
- return Object.assign({}, {
- counter: state.counter - action.number,
- });
- default: return state;
- }
- }
- // 創建store
- const store = createStore(countReducer);
- // 訂閱store的修改
- const unsubscribe = store.subscribe(function log() {
- console.log(store.getState());
- });
- // 通過dispatch action來改變state
- store.dispatch(incrementCreator(5)); //Object {counter: 5}
- store.dispatch(decrementCreator(4)); //Object {counter: 1}
- // 取消訂閱
- unsubscribe();