1. 程式人生 > >深入淺出React+Redux(三:Flux單向資料流,相關程式碼在github flux分支)

深入淺出React+Redux(三:Flux單向資料流,相關程式碼在github flux分支)

前言

通過上章,我們能感覺到僅僅通過prop和state 管理React大型專案,簡直是個巨大,恐怖乃至不可完成的挑戰。因為社群和個人喜愛還是推薦Redux做專案的狀態管理。但是作為單向資料流鼻祖的Flux,也是讀者需要整理下區別的。

(一)前端MVC 框架的缺陷

MVC框架是業界廣泛接受的一種前端應用架構型別,它把應用分成三個部分:

  • Model (模型)負責管理資料 ,大部分業務邏輯也應該放在 Model 中;
  • View (檢視)負責渲染使用者介面,應該避免在 View 中涉及業務邏輯;
  • Controller (控制器)負責接受使用者輸入 根據使用者輸入呼叫對應的 Model 部分邏輯,把產生的資料結果交給 View 部分,讓 View 渲染出必要的輸出 。

MVC幾個部分組成部分和請求的關係圖

理想中

這種將一個應用劃分為多個元件,就是“分而治之”。但是現實專案足夠大後,實際情況是什麼樣子的呢。如下圖

實際中

實際上。這是客戶端區分伺服器的地方,在伺服器mvc依舊是霸主地位,它的一個完整請求是以Controller中的request發起,response結束(其實本身資料流也類似單向)·。

但是客戶端,總是允許View 和Model 可以直接通訊。就會造成上面圖中的情況。

(二)FlUX基本概念

Facebook使用 Flux 框架來代替原有的 MVC 框架,他們提出的 Flux 框架大致結構如下圖。

Flux架構

首先,Flux將一個應用分成四個部分。

  1. Dispatcher ,處理動作分發,維持 Store 之間的依賴關係;
  2. Store ,負責儲存資料和處理資料相關邏輯 ;
  3. Action ,驅動 Dispatcher 的 JavaScript 物件;
  4. View ,檢視部分,負責顯示使用者介面。

Flux和MVC對比

Flux 的 Dispatcher 相當於 MVC 的Controller, Flux 的 Store 相當於 MVC 的 Model, Flux 的 View 當然就對應 MVC 的 View了,至於多出來的這個 Action ,可以理解為對應給 MVC 框架的使用者請求 。

當需要擴充應用所能處理的“請求”時, MVC 方法就需要增加新的 Controller

,而對於 Flux 則只是增加新的 Action

(三)FlUX 簡單demo

安裝依賴

$ yarn add flux --dev

(1)Dispatcher

首先,我們要創造一個 Dispatcher,

src/appDispatcher/index.js。創造這個唯一 的Dispatcher 物件

/**
 * @component appDispatcher
 * @description 全域性Dispatcher
 * @time 2018/1/16
 * @author jokerXu
 */
import {Dispatcher} from 'flux';

export default new Dispatcher();

Dispatcher 存在的作用,就是用來派發 action ,接下來我們就來定義應用中涉及的 action。

(2)Action

action 顧名思義代表一個“動作”,不過這個動作只是一個普通的 JavaScript 物件,代表一個動作的純資料,類似於 DOM API 中的事件( event ) 。 甚至,和事件相比, action其實還是更加純粹的資料物件,因為事件往往還包含一些方法,比如點選事件就有
preventDefault 方法,但是 action 物件不自帶方法,就是純粹的資料 。

作為管理, action 物件必須有一個名為 type 的欄位,代表這個 action 物件的型別,
為了記錄日誌和 debug 方便,這個 type 應該是字串型別 。

定義 action 通常需要兩個檔案,一個定義 action 的型別,一個定義 action 的構造函
數(也稱為 action creator ) 。 分成兩個檔案的主要原因是在 Store 中會根據 action 型別做
不同操作,也就有單獨導人 action 型別的需要 。

首先在src/actionTypes/demo.js中定義型別。

/**
 * @component actionTypes
 * @description demo動作型別
 * @time 2018/1/22
 * @author ***
 */

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';

上面程式碼表示,執行兩次操作,一個事點選”+”,一個事點選”-“。

然後在src/actions/demo.js中定義動作

/**
 * @component actions
 * @description demo動作
 * @time 2018/1/22
 * @author ***
 */

import * as ActionTypes from '../actionTypes/demo';
import AppDispatcher from '../appDispatcher';

export const increment = (counterCaption) => {
    AppDispatcher.dispatch({
        type: ActionTypes.INCREMENT,
        value: counterCaption
    })
};


export const decrement = (counterCaption) => {
    AppDispatcher.dispatch({
        type: ActionTypes.DECREMENT,
        value: counterCaption,
    })
};

雖然出於業界習慣,這個檔案被命名為 Actions. ,但是要注意裡面定義的並不是
action 物件本身,而是能夠產生並派發 action 物件的函式 。

我們匯出了兩個 action 建構函式 increment 和 decrement,當這兩個函式被調
用的時候,創造了對應的 action 物件,並立即通過 AppDispatcher.dispatch 函式派發出去 。

(3)Store

一個 Store 也是一個物件,這個物件儲存應用狀態,同時還要接受 Dispatcher 派發的
動作,根據動作來決定是否要更新應用狀態 。

我們創造兩個 Store ,一個是為 Counter 元件服務的 CounterStore ,另 一個就是為總
數服務的 SummaryStore 。

(1)定義src/stores/counterStore.js

/**
 * @component stores
 * @description demo的數量store
 * @time 2018/1/22
 * @author jokerXu
 */
import * as ActionTypes from '../actionTypes/demo';
import {EventEmitter} from 'events';

const CHANGE_EVENT = 'changed';

const counterValues = {
    'First': 0,
    'Second': 10,
    'Third': 20,
};

const CounterStore = Object.assign({}, EventEmitter.prototype, {
    getCounterValues: function () {
        return counterValues;
    },
    emitChange: function () {
        this.emit(CHANGE_EVENT);
    },
    addChangeListener: function (callback) {
        this.on(CHANGE_EVENT, callback)
    },
    removeChangeListener: function (callback) {
        this.removeListener(CHANGE_EVENT, callback);
    }
});

export default CounterStore;

當 Store 的狀態發生變化的時候, 需要通知應用的其他部分做必要的響應 。 在我們
的應用中,做出響應的部分當然就是 View 部分,但是我們不應該硬編碼這種聯絡,應
該用訊息的方式建立 Store 和 View 的聯絡 。 這就是為什麼我們讓 CounterStore 擴充套件了
EventEmitter.prototype ,等於讓 CounterStore 成了 EventEmitter 物件, 一個 EventEmitter
例項物件支援下列相關函式 。

  • emit 函式,可以廣播一個特定事件,第一個引數是字串型別的事件名稱 ;
  • on 函式,可以增加一個掛在這個 EventEmitter 物件特定事件上的處理函式,第一個引數是字串型別的事件名稱,第二個引數是處理函式;
  • removeListener 函式, 和 on 函式做的事情相反, 刪除掛在這個 EventEmitter 物件特定事件上的處理函式,和 on 函式一樣, 第一個引數是事件名稱 ,第二個引數是處理函式 。

對於 CounterStore 物件, emitChange 、 addChangeListener 和 removeChangeListener 函
數就是利用 EventEmitter 上述的三個函式完成對 CounterStore 狀態更新的廣播、新增監昕
函式和刪除監昕 函式等操作 。

CounterStore 函 數還提供一個 getCounterValues 函式,用於讓應用中其他模組可以讀
取當前的計數值,當前的計數值儲存在檔案模組級的變數 counterValues 中 。

接下來將 Store 只有註冊到 Dispatcher 例項上才能真正發揮作用

import AppDispatcher from '../appDispatcher';

CounterStore.dispatchToken = AppDispatcher.register((action) => {
    if (action.type === ActionTypes.INCREMENT) {
        counterValues[action.value]++;
        CounterStore.emitChange();
    } else if (action.type === ActionTypes.DECREMENT) {
        counterValues[action.value]--;
        CounterStore.emitChange();
    }
});

這是最重要 的 一 個 步 驟, 要 把 CounterStore 注 冊到全域性唯 一 的 Dispatcher 上 去。Dispatcher 有一個函式叫做 register ,接受一個回撥函式作為引數 。 返回值是一個 token ,這個 token 可以用於 Store 之間的同步。

現在我們來仔細看看 register 接受的這個回撥函式引數,這是 Flux 流程中最核心的部分,當通過 register 函式把一個回撥函式註冊到 Dispatcher 之後 , 所有派發給 Dispatcher的 action 物件 ,都會傳遞到這個回撥函式中來 。

比如通過 Dispatcher 派發一個動作,程式碼如下:

AppDispatcher.dispatch ({
    type: ActionTypes.INCREMENT,
    counterCaption: 'First '
});

根據不同的 type ,會有不同的操作,所以註冊的回撥函式很自然有一個模式,就是
函式體是一串 if-else 條件語句或者 switch 條件語句,而條件語句的跳轉條件,都是針對
引數 action 物件的 type 欄位

(2)定義src/stores/summaryStore.js

SummaryStore 也有 emitChange 、 addChangeListener 還有 removeChangeListener 函式,
功能一樣也是用於通知監昕者狀態變化,這幾個函式的程式碼和 CounterStore 中完全重複,
不同點是對獲取狀態函式的定義,

/**
 * @component stores
 * @description demo的總數store
 * @time 2018/1/22
 * @author jokerXu
 */

import CounterStore from './counterStore';

import * as ActionTypes from '../actionTypes/demo';
import {EventEmitter} from 'events';

const CHANGE_EVENT = 'changed';

function computeSumrnary(counterValues) {
    let summary = 0;
    for (const key in counterValues) {
        if (counterValues.hasOwnProperty(key)) {
            summary += counterValues[key];
        }
    }
    return summary;
}

const SumrnaryStore = Object.assign({}, EventEmitter.prototype, {
    getSummary: function () {
        return computeSumrnary(CounterStore.getCounterValues());
    },
    emitChange: function () {
        this.emit(CHANGE_EVENT);
    },
    addChangeListener: function (callback) {
        this.on(CHANGE_EVENT, callback)
    },
    removeChangeListener: function (callback) {
        this.removeListener(CHANGE_EVENT, callback);
    }
});

export default SumrnaryStore;

可以注意到, SummaryStore 並沒有儲存自己的狀態,當 getSummary 被呼叫時,它
是直接從 CounterStore 裡獲取狀態計算的 。

可見,雖然名為 Store ,但並不表示一個 Store 必須要儲存什麼東西, Store 只是提供
獲取資料的方法,而 Store 提供的資料完全可以另一個 Store 計算得來 。

SummaryStore 在 Dispatcher 上註冊的回撥函式也和 CounterStore 很不一樣,程式碼
如下:

import AppDispatcher from '../appDispatcher';

SumrnaryStore.dispatchToken = AppDispatcher.register((action) => {
    if ((action.type === ActionTypes.INCREMENT) ||
        (action.type === ActionTypes.DECREMENT)
    ) {
        AppDispatcher.waitFor([CounterStore.dispatchToken]);
        SumrnaryStore.emitChange();
    }
});

這裡使用了 waitFor 函式,這個函式解決的是下面描述的問題。

即使 Flux 按照 register 呼叫的順序去呼叫各個回撥函式,我們也完全無法把握各個Store 哪個先裝載從而呼叫 register 函式 。 所以,可以認為 Dispatcher 呼叫回撥函式的順序完全是無法預期的,不要假設它會按照我們期望的順序逐個呼叫 。

Dispatcher 的 waitFor 可以接受一個數組作為引數,陣列中每個元素都是一個 Dispatcherregister 函式的返回結果,也就所謂的 dispatchToken 。 這個waitFor 函式告訴 Dispatcher,
當前的處理必須要暫停,直到 dispatchToken 代表的那些已註冊回撥函式執行結束才能繼續 。

注意

Dispatcher 的 register 函式,只提供了註冊一個回撥函式的功能,但卻不能讓呼叫者在 register 時選擇只監聽某些 action,換句話說,每個 register 的呼叫者只能這樣請求:“ 當有任何動作被派發時,請呼叫我 。 ”但不能夠這麼請求:“當這種型別還有那種型別的動作被派發的時候,請呼叫我 。 ”

當一個動作被派發的時候, Dispatcher 就是簡單地把所有註冊的回撥函式全都呼叫一
遍,至於這個動作是不是對方關心的, Flux 的 Dispatcher 不關心,要求每個回撥函式去
鑑別 。

看起來,這似乎是一種浪費,但是這個設計讓 Flux 的 Dispatcher 邏輯最簡單化,
Dispatcher 的責任越簡單,就越不會出現問題 。 畢竟,由回撥函式全權決定如何處理 action
物件,也是非常合理的 。

(4)Views

存在於 Flux 框架中的 React 元件需要實現以下幾個功能:

  • 建立時要讀取 Store 上狀態來初始化元件內部狀態;
  • 當 Store 上狀態發生變化時,元件要立刻同步更新內部狀態保持一致;
  • View 如果要改變 Store 狀態,必須而且只能派發 action 。

新增src/views/ControlPanel/Summary.js

/**
 * @component Summary
 * @description
 * @time 2018/1/22
 * @author ***
 */

import React, { Component } from 'react';

import SummaryStore from '../../stores/summaryStore.jsx';

class Summary extends Component {

    constructor(props) {
        super(props);
        this.state = {
            sum: SummaryStore.getSummary()
        }
    }

    componentDidMount() {
        SummaryStore.addChangeListener(this.onUpdate);
    }

    componentWillUnmount() {
        SummaryStore.removeChangeListener(this.onUpdate);
    }

    onUpdate=() => {
        this.setState({
            sum: SummaryStore.getSummary()
        })
    }

    render() {
        return (
            <div>Total Count: {this.state.sum}</div>
        );
    }
}

export default Summary;

只要 CounterStore 發生變化, Counter 元件的 onChange 函式就會被呼叫。與 componentDidMount 函式中監昕事件相對應,在componentWillUnmount 函式中刪除了這個監昕 。

修改src/views/ControlPanel/index.js

import React, { Component } from 'react';
import Counter from './Counter';
import Summary from './Summary';

class ControlPanel extends Component {

    constructor (props) {
        super(props);
        this.initValues = [
            {
                title: 'First',
            },{
                title: 'Second',
            },{
                title: 'Third',
            }
        ];
    }

    // 遍歷子元件
    mapCounter=() =>{
        return this.initValues.map((value,index)=>{
            return <Counter key={index} caption={value.title} />;
        })
    };

    render() {
        return (
            <div>
                {this.mapCounter()}
                <hr/>
                <Summary />
            </div>
        )
    }
}

export default ControlPanel;

修改src/views/ControlPanel/Counter.jsx

/**
 * @component Counter
 * @description 子元件
 * @time 2018/1/15
 * @author ***
 */
import React, { Component } from 'react';
import PropTypes from 'prop-types';

import CounterStore from '../../stores/counterStore';
import * as Actions from '../../actions/demo';

class Counter extends Component{

    constructor(props){
        super(props);

        this.state= {
            count: CounterStore.getCounterValues() [props.caption],
        }
    }
    clickIncrementHandler=() =>{
        Actions.increment(this.props.caption);
    };
    clickDecrementHandler=() =>{
        Actions.decrement(this.props.caption);
    };
    componentDidMount( ) {
        CounterStore.addChangeListener(this.onChange) ;
    }
    componentWillUnmount() {
        CounterStore.removeChangeListener(this.onChange);
    }
    onChange = () => {
        const newCount = CounterStore.getCounterValues()[this.props.caption];
        this.setState({count: newCount});
    }
    shouldComponentUpdate(nextProps , nextState) {
        return (nextProps.caption !== this.props.caption) || (nextState.count !==this.state.count);
    }

    render(){
        const buttonStyle= {
            marginRight: '15px',
        };
        const { caption }= this.props;
        const { count }= this.state;
        return (
            <div>
                <button style={buttonStyle} onClick={this.clickIncrementHandler}>+</button>
                <button style={buttonStyle} onClick={this.clickDecrementHandler}>-</button>
                <span>{caption} count: {count}</span>
            </div>
        );
    }
}

Counter.propTypes= {
    caption: PropTypes.string.isRequired,
};

export default Counter;

(四)Flux 的優勢

回顧一下完全只使用 React 實現的版本,應用的狀態資料只存在於 React 元件之中,每個元件都要維護驅動自己渲染的狀態資料,單個元件的狀態還好維護,但是如果多個元件之間的狀態有關聯,那就麻煩了 。 比如 Counter 元件和 Summary 元件, Summary 元件需要維護所有 Counter 元件計數值的總和, Counter 元件和 Summary 分別維護自己的狀態,如何同步 Summary 和 Counter 狀態就成了問題, React 只提供了 props 方法讓元件之間通訊,元件之間關係稍微複雜一點,這種方式就顯得非常笨拙 。

Flux 的架構下,應用的狀態被放在了 Store 中, React 元件只是扮演 View 的作用,
被動根據 Store 的狀態來渲染 。 在上面的例子中, React 元件依然有自己的狀態,但是已
經完全淪為 Store 元件的一個對映,而不是主動變化的資料 。

Flux 帶來了哪些好處呢?最重要的就是“單向資料流”的管理方式 。

在 Flux 的理念裡,如果要改變介面,必須改變 Store 中的狀態,如果要改變 Store 中
的狀態,必須派發一個 action 物件,這就是規矩 。 在這個規矩之下,想要追溯一個應用
的邏輯就變得非常容易 。

我們已經討論過 MVC 框架的缺點 MVC 最大的問題就是無法禁絕 View 和 Model 之
間的直接對話,對應於 MVC 中 View 就是 Flux 中的 View ,對應於 MVC 中的 Model 的就
是 Flux 中的 Store ,在 Flux 中, Store 只有 get 方法,沒有 set 方法,根本可能直接去修改
其內部狀態, View 只能通過 get 方法獲取 Store 的狀態,無法直接去修改狀態,如果 View
想要修改 Store 狀態的話,只有派發一個 action 物件給 Dispatcher。這看起來是一個“限制”,但卻是一個很好的“限制”,禁絕了資料流淚亂的可能 。簡單說來,在 Flux 的體系下,驅動介面改變始於一個動作的派發,別無他法 。

(四)Flux 的不足

(1). Store 之間依賴關係

在 Flux 的體系中,如果兩個 Store 之間有邏輯依賴關係,就必須用上 Dispatcher 的
waitFor 函式 。 而 dispatchToken 的產生,當然是 CounterStore 控制的,換句話說,要這樣設計:

  • CounterStore 必須要把註冊回撥函式時產生的 dispatchToken 公之於眾;
  • SummaryStore 必須要在程式碼裡建立對 CounterStore 的 dispatchToken 的依賴 。

雖然 Flux 這個設計的確解決了 Store 之間的依賴關係,但是,這樣明顯的模組之間
的依賴,看著還是讓人感覺不大舒服,畢竟,最好的依賴管理是根本不讓依賴產生 。

(2). 難以進行伺服器端渲染

在 Flux 的體系中,有一個全域性的 Dispatcher ,然後每一個 Store 都是一個全域性唯一
的物件,這對於瀏覽器端應用完全沒有問題,但是如果放在伺服器端,就會有大問題。

和一個瀏覽器網頁只服務於一個使用者不同,在伺服器端要同時接受很多使用者的請求,
如果每個 Store 都是全域性唯一的物件,那不同請求的狀態肯定就亂套了 。

並不是說 Flux 不能做伺服器端渲染,只是說讓 Flux 做伺服器端渲染很困難,實際
上, Facebook 也說的很清楚, Flux 不是設計用作伺服器端渲染的,他們也從來沒有嘗試
過把 Flux 應用於伺服器端。

(3). Store 混雜了邏輯和狀態

Store 封裝了資料和處理資料的邏輯,用面向物件的思維來看,這是一件好事,畢
竟物件就是這樣定義的 。 但是,當我們需要動態替換一個 Store 的邏輯時,只能把這個
Store 整體替換掉,那也就無法保持 Store 中儲存的狀態 。

最後

我們把 Flux 看作一個框架理念的話, Redux 是 Flux 的一種實現,除了 Redux 之外,
還有很多實現 Flux 的框架,比如 Reflux, Fluxible 等,毫無疑問 Redux 獲得的關注最多,
這不是偶然的,因為 Redux 有很多其他框架無法比擬的優勢 。