應用數據流狀態管理框架Redux簡介、設計思想、核心概念及工作流

【本文源址:http://blog.csdn.net/q1056843325/article/details/54784109 轉載請添加該地址】

前幾天給大家談了談React
不過它只是一個側重於UI的框架
只能算作是MVC中的V(View視圖)
而且只是DOM的一個抽象層,不是Web應用完整解決方案
如果僅僅用它構建大型項目
你會非常的吃力

簡介

14年,Facebook提出Flux架構意圖解決這個問題
15年,Dan Abramov將 Flux 與函數式編程相結合,創造了Redux,由於簡單易學就開始流行起來
16年,Dan Abramov被facebook挖走了

Redux體積很小,如果刪掉源碼的空行和註釋,連500行代碼都不到
別看它小(壓縮版7KB),卻非常有用

優點

使用它構建web應用有如下優點:

  • 預測
    始終有一個準確的數據源,就是store, 對於如何將actions以及應用的其他部分和當前的狀態同步可以做到絕不混亂。
  • 維護
    具備可預測結果的性質和嚴格的組織結構讓代碼更容易維護。
  • 組織
    對代碼應該如何組織更加嚴苛,這使代碼更加一致,對團隊協作更加容易。
  • 測試
    編寫可測試代碼的首要準則就是編寫可以僅做一件事並且獨立的小函數。Redux的代碼幾乎全部都是這樣的函數:短小、純粹、分離。
  • 服務端渲染
    可以帶來更好的用戶體驗並且有助於搜索引擎優化,尤其是對於首次渲染。僅僅是把服務端創建的store傳遞給客戶端就可以。
  • 開發者工具
    開發者可以實時跟蹤在應用中正在發生的一切,從actions到狀態的改變。
  • 社區與生態圈
    存在很多支持Redux的社區,使它能夠吸引更多的人來使用。

可能有同學會說,這不是和百度百科一樣麼
這應該不能算抄吧,其實百度百科redux的詞條是我建的o( ̄▽ ̄)d
不過編輯的蠻差勁的,希望各位有時間的時候能補充詞條讓更多人瞭解

使用場景

還要強調的一點就是,不是任何時候都需要用它
如果我們用React遇到瓶頸了,或許我們才需要用到Redux
一般情況它都是配合React使用的(因爲React用state描述界面,Redux控制state,配合天衣無縫)
不過也可以配合其他框架(甚至原生JavaScript)

阮大神在這方面總結的很詳細
這些情況不需要用Redux

  • 用戶的使用方式非常簡單
  • 用戶之間沒有協作
  • 不需要與服務器大量交互,也沒有使用 WebSocket
  • 視圖層(View)只從單一來源獲取數據

下列情況建議使用Redux

  • 用戶的使用方式複雜
  • 不同身份的用戶有不同的使用方式(比如普通用戶和管理員)
  • 多個用戶之間可以協作
  • 與服務器大量交互,或者使用了WebSocket
  • View要從多個來源獲取數據

組件角度考慮的話,Redux的使用場景

  • 某個組件的狀態,需要共享
  • 某個狀態需要在任何地方都可以拿到
  • 一個組件需要改變全局狀態
  • 一個組件需要改變另一個組件的狀態

設計原理

學Redux的時候就覺得很親切,但不知道親切在哪兒
後來知道了,這不就是命令模式麼
而這個命令command對象就是store

命令模式的應用場景就是:有時需要向某些對象發送請求,但是不知道請求的接收者是誰,
也不知道被請求的操作是什麼,此時希望用一種鬆耦合的方式來設計軟件,
使得請求發送者和請求接收者能夠消除彼此之間的耦合關係

而我們經常會遇到這樣的問題
觸發事件後,必須向某些負責具體行爲的對象發送請求(接受者receiver)
但還不知道接收者是什麼,也不到它要做什麼,這是我們就需要命令對象command解耦

所以說白了Redux就是一種設計模式——命令模式
(可見設計模式有多重要,有時間真的該深入研究一下)

設計思想

Redux設計思想就兩句話,切記
(再次引用阮大神的總結)

  • Web應用是一個狀態機(state machine),視圖與狀態一一對應
  • 所有狀態(state)保存在一個對象中(store)

核心概念

其實Redux架構並不難
文件小,概念也沒有多少
最最重要的就是這個

單一狀態樹

Redux中單一狀態樹這個概念很重要
什麼是單一狀態樹呢?(也叫單一數據源)
所有的狀態state對象(數據)都以樹的形式儲存唯一的store中
頁面中的任何變動,首先都要去改變狀態樹(store),然後以某種形式呈現到頁面
還記得剛剛瞭解的Redux設計思想嗎
一個視圖對應一個狀態對象

舉個例子
比如我們的一個state對象

{
    text: 'a text',
    color: 'red'
}

它可能展示一個這樣的視圖

<p style="color: red;">a text<p>

現在狀態改變了,變成了這個樣子

{
    text: 'another text',
    color: 'green'
}

狀態改變了,視圖也隨之改變

<p style="color: green;">another text<p>

基本概念

Redux核心的概念就三個:Action、Reducer、Store

Action

Action是一個對象,表示頁面發生的事情,也就相當於事件

內部其實就是簡單的具有一個type屬性(常爲常量)的JS對象,
描述了Action的類型以及負載信息
比如上面我們視圖變化的例子

//Action
{
    type: 'TEXT_CHANGE',
    newText: 'another text',
    newColor: 'red'
}

Action Creators

考慮到對它的複用,Action可以通過生成器(Action Creators)來創建
其實它就是返回Action對象的函數(我們自定義的函數)
參數可以適情況而定

//Action Creators
function change(text, color){
    return {
        type: 'TEXT_CHANGE',
        newText: text,
        newColor: color
    }
}

Reducer

Reducer是一個函數,用於修改我們的單一狀態樹

還記得ES5的Array.prototype.reduce()函數麼
我認爲reduce中文釋義中最接近這個函數功能的應該是“濃縮”
這個濃縮函數接收一個回調讓我們將多個值濃縮爲一個值
我們的reuder就可以作爲(包含多個action對象的)數組的回調函數

Pure Function

Reducer是一個純函數(Pure Function)
(相同的輸入會得到同樣的輸出)

純函數(Pure Function)
概念源於函數式編程

  • 不能改寫參數
  • 不能調用系統I/O的API
  • 不能調用Math.random()或Date.now()等不純方法
    (相同的輸入要得到同樣的輸出)

它獲取應用的state和action作爲參數,通過判斷action的type屬性返回一個新的state
僞代碼表示就是:state + action => state
我們例子中的Reducer可以這樣來寫

//Reducer
const initState = {
    text: 'a text',
    color: 'red'
}
function reducer(state = initState, action){
    switch(action.type){
        case 'TEXT_CHANGE':
            return {
                text: action.newText,
                color: action.newColor
            }
        default:
            return state;
    }
}

由於純函數這個條件的限制
所以我們不能改寫參數中的state
所以比如說我們的state要表示列表
於是使用了數組來表示state
那我們就不能使用state.push()
如果state是對象也是同樣的道理
可選方案如下:

  • 數組
    • 使用ES3的arr.concat(),得到新數組後返回
    • 使用ES6的展開操作符[…state, newItem],得到新數組後返回
  • 對象
    • 使用ES6的Object.assign({ }, state, changeObj),得到新對象後返回
    • 使用ES6的展開操作符{…state, newState},得到新對象後返回

只要記住一點就好了,在reducer中不能改變參數state

combineReducers( )

上面我們說到了,整個應用的數據都保存在一個store中,就導致store非常龐大
可想而知,reducer也會十分巨大
不過,Redux庫爲我們提供了combineReducers( )函數
讓我們可以自定義很多reducer函數,然後通過它來合併成一個大reducer
參數就是一個對象

import {combineReducers} from 'redux';
const reducer = combineReducers({a,b,c});

這樣利用ES6的語法,會讓state的屬性名與reducer的函數名相同
如果不同名的話,可以這樣寫

const reducer = combineReducers({
  a: reducerA,
  b: reducerB,
  c: reducerC
});

下面模擬實現一個combineReducers( )函數
內部使用了數組reduce方法,忘了的同學戳這裏:傳送門

const combineReducers = reducersObj => {
  return (state = {}, action) => { //執行combineReducers後返回一個大Reducer
    return Object.keys(reducersObj).reduce(
      (obj, key) => {
        obj[key] = reducersObj[key](state[key], action);
        return obj; 
      },
    {});
  };
};

/*
合成後的Reducer返回一個對象
{
  a: reducerA(state.a, action),
  b: reducerB(state.b, action),
  c: reducerC(state.c, action)
}
*/

Store

Store是一個對象,它保存應用的狀態並提供一些方法來存取狀態,分發狀態以及註冊監聽
全部state由一個Store來表示,Store維持着應用的state
Store 把Reducer和Action聯繫到一起

API

API如下:

  • 獲取狀態:store.getState( )
    • 獲取當前時間點的數據集合state
  • 更新狀態:store.dispatch( action )
    • 修改狀態的唯一方式就是dispatch一個action,store接收到action後自動調用reducer
  • 綁定監聽:store.subscribe( listener )
    • 一旦狀態改變,就執行綁定的函數;方法返回一個函數,調用可以解除監聽
  • 改變reducer:replaceReducer( new-reducer )
    • 替換創建store時的reducer頁面,比如頁面跳轉時使用(新的API,好像好少用)

API的使用也非常簡單
比如我們試着這樣做

store.subscribe(()=>console.log(store.getState()));
store.dispatch(change('another text','green'));
// 輸出:{color:'green',text:'another text'}

首先我們(subscribe)綁定了一個listener
這個listener會讓應用state更新後,就輸出當前state(getState)
最後(dispatch)更新狀態

createStore( )

我們可以通過redux庫中的createStore( )方法來創建Store
參數如下:

  • reducer(必選):
    • 必須爲store指定一個reducer函數
  • preloadedState(可選):
    • 整個應用state初始值,通常由服務器發出
    • 如果選擇使用這個參數,就會覆蓋reducer的默認state初始值
  • enhancer (可選):
    • 一個函數,如果選擇使用它,則會使用一個強化版的store creator
    • 源碼return enhancer(createStore)(reducer, preloadedState)
import {createStore} from 'redux';
const store = createStore(reducer);
//或者乾脆直接導入API 
//let {getState,dispatch,subscribe} from createStore(reducer)

瞭解了它的功能,我們可以試着模擬一個簡單的createStore( )函數

const createStore = (reducer, preloadedState, enhancer) => {
  if(enhancer){
    return enhancer(createStore)(reducer, preloadedState);
  } /*如果有enhancer,就使用強化版store creator*/
  let state = preloadedState, /*有preloadedState就設置爲默認值,否則爲undefined*/
      listeners = []; /*綁定的listener數組*/
  const getState = () => state;
  const dispatch = (action) => {
    state.reducer(state, action);
    listeners.forEach(listener => listener());
  } /*dispatch後,調用reducer更新state,一次執行listener*/
  const subscribe = (listener) => {
    listeners.push(listener);
    let index = listeners.length;
    return () => listeners.splice(index, 1);
  } /*subscribe添加listener,返回一個unsubscribe函數*/
  return {
    getStore,
    dispatch,
    subscribe
  }
}

這個只是幫助大家理解,所以沒有寫太細
更詳細的可以參考源碼

工作流

如果瞭解了核心概念後一臉懵逼不要緊
繼續往下看就懂了
使用React配合Redux構建項目的工作流如下:

Store由Redux的API-createStore(reducer)創建
頁面觸發事件由Action Creators返回action轉發給Store
Store再將當前state和收到的action轉給Reducer
Reducer處理後返回一個新state反饋給Store
Store管理的state改變了
就會引發React組件的重新渲染

再通俗一點兒
用戶發出Action,Reducer更新state,View重繪


是不是發現Redux也就這點兒東西,豁然貫通 ︿( ̄︶ ̄)︿
(其實這只是最最基本的Redux,還有很多有用的API,理解容易,用好難)
當然如果僅僅是理解了,也只是紙上談兵
由於本文只是淺談Redux
就講一個小小demo加深一下對Redux的理解

小實例

這個實例就是一個簡單的手動計數器
既然由Redux來管理頁面的state
React組件中就不必使用state了
我們使用React的時候都是利用props、state改變而觸發內部的render重繪
但現在我們利用React+Redux
就要自定義一個render函數再利用store.subscribe()綁定
這樣store管理的state改變,就會觸發我們自定義的render函數重繪頁面

結構以及綁定的事件
事件處理函數也很簡單,將action傳遞給store

const Counter = React.createClass({
    reduceHandler(){
        store.dispatch({type: 'REDUCE'});
    },
    addHandler(){
        store.dispatch({type: 'ADD'});
    },
    render(){
        return (
            <div>
                <p>{this.props.value}</p>
                <button onClick={this.reduceHandler}>-</button>
                <button onClick={this.addHandler}>+</button>
            </div>
        )
    }
});

構建reducer和store

const reducer = (state = 0, action) => {
  switch (action.type) {
    case 'ADD': return state + 1;
    case 'REDUCE': return state - 1;
    default: return state;
  }
};
const store = createStore(reducer);

創建渲染listener並綁定,每次store改變,觸發重繪

const render = () => {
  ReactDom.render(
    <Counter value={store.getState()}/>,
    document.getElementById('root')
  );
};
store.subscribe(render);

不要忘了首次需要進行手動渲染
否則頁面什麼都沒有

render();

==主頁傳送門==

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章