React 升級:Redux

前言

近期接觸React項目,學到許多新知識點,網上教程甚多,但大多都把知識點分開來說,初學者容易陷入學習的誤區,摸不着頭腦,本人在學習中也遇到許多坑。此篇文章是筆者看過的寫得比較詳細的具體的,同時能把全部的知識點統一串聯起來,很是適合初學者學習。因爲文檔是英文版,考慮到大夥英語水平各不相同,故作這次翻譯,一來深化本身對Redux的體系認知,二來方便你們理解閱讀。javascript

因爲文中出現大量技術名詞,應適當結合原文進行閱讀,原文鏈接:
《Leveling Up with React: Redux》 By Brad Westfall On March 28, 2016css

此篇教程是原文做者一系列教程的最後一篇,這裏只對該篇進行翻譯,剩餘的幾篇有時間會繼續進行翻譯,對於文中出現的翻譯錯誤,歡迎你們積極指正。html


本教程是 Brad Westfall 三部分系列教程的最後一篇。咱們將學習如何有效地管理狀態,使其跨越整個應用程序,而且能夠在沒有嚴重複雜度的狀況下進行衡量。在React的學習道路上咱們已經走了這麼遠,如今是時候來跨過終點,得到這個物超所值的所有成長曆程。java

系列文章

Redux 是一個用來管理JavaScript應用中 data-state(數據狀態)和UI-state(UI狀態)的工具,對於那些隨着時間推移狀態管理變得愈來愈複雜的單頁面應用(SPAs)它是比較理想的,同時,它又是和框架無關的,所以,儘管它是提供給React使用的,但它也能夠結合Angular 或者 jQuery來使用。git

另外,它的設想來自一個叫作「時間旅行」的實驗,這是真實的,咱們後面會講到。es6

正如咱們前面的教程所提到的,React 在組件之間流通數據.更確切的說,這被叫作「單向數據流」——數據沿着一個方向從父組件流到子組件。因爲這個特性,對於沒有父子關係的兩個組件之間的數據交流就變得不是那麼顯而易見。github

React 不推薦組件對組件直接交流的這種方式,儘管它確實有一些特徵能夠支持這個方法,但在許多組件之間進行直接的組件對組件的交流被認爲是很差的作法,由於這樣會容易出錯,而且致使spaghetti code —— 過期的代碼, 很難維護。

React 提供了一個建議,可是他們但願你能本身來實現它。這裏是React官方文檔裏的一段話:

想讓兩個沒有父子關係的組件進行交流,你能夠經過設置你本身的全局事件機制…… Flux 模式就是其中一個可行的方案

這裏 Redux 就排上用場了。Redux提供了一個解決方案,經過將應用程序全部的狀態都存儲在一個地方,叫作「store」。而後組件就能夠「dispatch」狀態的改變給這個store,而不是直接跟另外的組件交流。全部的組件都應該意識到狀態的改變能夠「subscribe」給store。

能夠把store想象成是應用程序中全部狀態改變的中介。隨着Redux的介入,全部的組件再也不相互直接交流,而是全部的狀態改變必須經過store這個單一的真實來源。

這和那些應用程序中不一樣的部分直接交流的策略有很大的不一樣。有時,那些策略被認爲是容易出錯和混亂的緣由:

有了Redux,全部的組件都從store中來獲取他們的狀態,變得很是清晰。一樣,組件狀態的改變都發送給了store,也很清晰。組件初始化狀態的改變只須要關心如何派發給store,而不用去關心一系列其它的組件是否須要狀態的改變。這就是Redux如何使數據流變得更簡單的緣由。

使用store來協調應用之間狀態改變的概念就是Flux模式。它是一種傾向單向數據流(好比 React)的設計模式。Redux像Flux,可是他們又有多少關係呢?

Redux is "Flux-like"

Flux 是一種模式,不像Redux那樣是能夠下載的工具,Redux 是受Flux模式,此外,它比較像Elm。這裏有許多有關於Redux和Flux之間比較的指南。它們中的大多數都會得出Redux就是Flux,或者Redux和Flux比較相似的結論,這取決於給Flux定義的規則到底有多嚴格。然而說到底,這些都可有可無。Facebook 很是喜歡而且支持Redux,這從它們僱傭了Redux的主要開發者 Dan Abramov 就能夠看出。

這篇文章假設你一點都不熟悉Flux的設計模式。不過若是你熟悉,你會注意到許多微小的不一樣,尤爲考慮到Redux的三大指導原則

1. 單一真實源

Redux只使用一個store來處理應用的狀態。由於全部的狀態都駐留在同一個地方,Redux稱這個爲單一真實源。

store中數據的結構徹底取決於你,但一般都是針對應用的一個深層嵌套的對象。

Redux的單一store方法是區分Flux多個store方法的最主要區別。

2. 狀態是隻讀的

Redux的文檔指出,惟一改變狀態的方法就是發出一個action,一個用來描述發生了什麼的對象。

這意味着應用不能直接改變狀態,相反,「actions」 被派發給store,用來描述一個改變狀態的意圖。

store對象本身有幾個小型的API,對應4個方法:

  • store.dispatch(action)

  • store.subscribe(listener)

  • store.getState()

  • replaceReducer(nextReducer)

因此你能夠看到,這裏沒有設置狀態的方法。所以,派發一個action是處理應用狀態更改的惟一辦法

var action = {
  type: 'ADD_USER',
  user: {name: 'Dan'}
};

// Assuming a store object has been created already
store.dispatch(action);

dispatch() 方法發送了一個對象給Redux,這個對象就被叫作action。這個action能夠被描述成一個攜帶了一個 type 屬性以及其它能夠被用來更新狀態的數據(在這個例子裏就是user)的有效負載。記住,在 type 屬性以後,這個action對象的設計徹底取決於你。

3. 全部的狀態改變使用的都是純函數

就像剛纔所描述的,Redux不容許應用直接改變狀態,而是用被分派的action來「描述」狀態改變或者改變狀態的意圖。而一個個Reducer就是你本身寫的函數,用來處理分派的action,事實上是它真正改變了狀態。

一個reducer接受當前的狀態(state)做爲參數,並且必須返回一個新的狀態才能改變以前的狀態。

// Reducer Function
var someReducer = function(state, action) {
  ...
  return state;
}

reducer 必須使用 「純」函數 , 一個能夠用如下這些特徵來描述的術語:

  • 沒有任何的網絡或數據庫請求操做

  • 返回的值僅依賴於參數

  • 參數必須是「不可改變的」,覺得着它們將不能被更改。

  • 調用具備相同參數集的純函數將始終返回相同的值

它們被稱爲「純」函數是由於它們什麼都不作僅僅返回一個基於參數的值。它們在系統的任何其餘部分都沒有反作用。

第一個 Redux Store

開始以前,須要先用 Redux.createStore() 建立一個store,而後將全部的reducer做爲參數傳遞進去,讓咱們看一下這個只傳遞了一個reducer的小例子:

// Note that using .push() in this way isn't the
// best approach. It's just the easiest to show
// for this example. We'll explain why in the next section.

// The Reducer Function
var userReducer = function(state, action) {
  if (state === undefined) {
    state = [];
  }
  if (action.type === 'ADD_USER') {
    state.push(action.user);
  }
  return state;
}

// Create a store by passing in the reducer
var store = Redux.createStore(userReducer);

// Dispatch our first action to express an intent to change the state
store.dispatch({
  type: 'ADD_USER',
  user: {name: 'Dan'}
});

上面的程序幹了些什麼呢:

  1. 這個store只由一個reducer建立。

  2. 這個reducer 初始化狀態的時候使用了一個空數組 。*

  3. 在被分派的這個action裏面使用了新的user對象。

  4. 這個reducer將這個新的user對象附加到state上,並將它返回,用來更新store。

*在這個例子裏reducer實際上被調用了兩次 —— 一次是在建立store的時候,一次是在分派action以後。

當store被建立以後,Redux當即調用了全部的reducer,而且將它們的返回值做爲初始狀態。第一次調用reducer傳遞了一個 undefined 給state。通過reducer內部的代碼處理以後返回了一個空數組給這個store的state做爲開始。

全部的reducer在每次action被分派以後都會被調用。由於reducer返回的狀態將會成爲新的狀態存儲在store中,因此 Redux老是但願全部的reducer都要返回一個狀態

在這個例子中,reducer第二次的調用發生在分派以後。記住,一個被分派的action描述了一個改變狀態的意圖,並且一般攜帶有數據用來更新狀態。這一次,Redux將當前的狀態(仍舊是空數組)和action對象一塊兒傳遞給了reducer。這個action對象,如今有了一個值爲‘ADD_USER’的type屬性, 讓reducer知道怎樣改變狀態。

咱們很容易就能將reducers和漏斗聯想起來,容許狀態經過他們。這是由於reducers老是接受和返回狀態用來更新store。

基於這個例子,咱們的store將會變成一個只有一個user對象的數組:

store.getState();   // => [{name: 'Dan'}]

不要改變狀態,複製它

在咱們上面的例子中這個reducer從技術上來說是可行的,可是它改變了狀態,這是一種很差的作法。儘管reducers 負責改變狀態,可是不該該直接改變「現有的狀態」。因此咱們不該該在reducer的state這個參數上使用.push()這個變異的方法

傳遞給reducer的參數應該被視爲不可改變的。換句話說,他們不該該被直接改變。咱們可使用不變異的方法好比.concat()來拷貝這個數組,而後咱們將拷貝的數組返回。

var userReducer = function(state = [], action) {
  if (action.type === 'ADD_USER') {
    var newState = state.concat([action.user]);
    return newState;
  }
  return state;
}

在這個新的reducer中,咱們添加了一個新的user對象做爲state參數的副本被改變和返回。當沒有添加新的用戶的時候,注意返回的是原始的state而不是它的拷貝。

有一大節關於不可變數據結構的最佳嘗試,咱們應該更多的去了解

你也許已經注意到初始化參數使用了ES2015的默認參數方法。到目前爲止,在這一些列的文章中,咱們一直避免使用ES2015來使你更專心於主題內容。然而,Redux和ES2015結合使用會變得很是完美。所以,咱們最終開始在這篇文章中使用ES2015。然而不用擔憂,每次採用新的ES2015的特性,咱們都會指出來而且解釋

多個reducer

上一個例子是一個很好的入門,可是大多數的應用都須要更復雜的state來知足整個應用。由於Redux僅使用一個store,因此咱們須要使用嵌套的對象來組織不一樣模塊的state。假設咱們的想要咱們的store相似於這種樣子:

{
  userState: { ... },
  widgetState: { ... }
}

整個應用對應的仍是 「一個store = 一個對象」,可是它嵌套了 userStatewidgetState 對象,能夠包含各類數據。這彷佛過於簡單了,可是實際上和一個真實的Redux store沒多少差異。

爲了建立具備嵌套對象的store,咱們須要定義每一塊的reducer:

import { createStore, combineReducers } from 'redux';

// The User Reducer
const userReducer = function(state = {}, action) {
  return state;
}

// The Widget Reducer
const widgetReducer = function(state = {}, action) {
  return state;
}

// Combine Reducers
const reducers = combineReducers({
  userState: userReducer,
  widgetState: widgetReducer
});

const store = createStore(reducers);

ES2015 提示! 在這個例子中四個主要的變量都不會被改變, 因此咱們將它們定義成常量. 同時咱們也使用了ES2015 modules and destructuring

combineReducers()容許咱們用不一樣的邏輯塊來描述store,將reducer分配給每個塊。如今,每個reducer返回的初始狀態會進入到它們store中各自對應的userState或者widgState塊。

有些很是重要的點須要注意,如今每個reducer中所傳遞的只是所有狀態中各自的部分,再也不像以前只有一個reducer時傳遞的是整個store的狀態。而後每一個reducer返回的狀態應用於它們各自的部分。

在分派以後調用的是哪個Reducer?

當咱們考慮每次action被分派的時候,把上面所有的reducer想一想成一個個漏斗會變得更加明瞭,全部的reducer都會被調用,都將有機會來更新各自的狀態:

我很當心地說「它們的」狀態是由於reducer的「當前狀態」參數和它的返回「更新」狀態僅僅影響到store中reducer裏面的部分。記住,像前面所說的,每個reducer只得到它們各自的狀態,而不是整個狀態。

Action 策略

實際上有大量的關於建立和管理action及其類型的策略。雖然它們都很棒,可是它們不像本文中的其餘一些信息那樣重要。爲了減小文章的篇幅,咱們整理了這些基本的action策略,你能夠在 GitHub repo上得到這一系列的策略。

不可變的數據結構

state的樣式由你本身決定: 它能夠是原始值,數組,對象,或者一個Immutable.js的數據結構。 惟一重要的部分就是你不能改變state對象,並且須要返回一個更改後的新對象 -- Redux 文檔

上面的陳訴說了不少,咱們已經在本教程中提到了這一點。若是咱們開始討論什麼是可變的什麼是不可變的的前因後果和利弊,咱們能夠在 《blog article's worth of information》找到更有價值的信息。因此事實上,我只是想突出一些要點。

開始前:

  • JavaScript的原始數據類型(Number, String, Boolean, Undefined, and Null) 已是不可變得了。

  • 對象、數組、函數是可變的。

有人說數據結構的可變性容易產生問題。由於咱們的store是有state對象和數組所組成,咱們須要實施一種策略來保持狀態不可變。

讓咱們假設須要改變一個state對象的屬性,這裏有三種方式:

// Example One
state.foo = '123';

// Example Two
Object.assign(state, { foo: 123 });

// Example Three
var newState = Object.assign({}, state, { foo: 123 });

第一個和第二個例子都改變了state對象。第二個例子是由於Object.assign()把全部的參數都合併到了第一個參數裏。但這也就是爲何第三個例子沒有改變state對象的緣由。

第三個例子將state的內容和{foo: 123}合併到了一個新的空對象中。這是一種常見的技巧,容許咱們建立一個state對象的副本,在副本上進行修改,本質上不會影響原始的state

對象的「擴展運算符」是保持state不可變的另外一種方式:

const newState = { ...state, foo: 123 };

有關於上述代碼究竟發生了什麼,爲何它對Redux是友好的詳細解釋,能夠參考這個主題的文檔

Object.assign() 和擴展運算符都是ES2015的特性。

總結來講,有許多方法能夠明確地保持對象和數組不可變。許多開發者使用第三方庫好比 seamless-immutableMori 甚至Facebook本身的Immutable.js 來達到這個目的。

我很是當心的選擇了一些相關的博客和教程。若是你不是很是明白不變性,能夠看一下上面給出的這些連接。這在Redux的學習中是一個很是重要的概念。

初始化狀態 和 時間旅行

若是你讀過文檔,你也許會注意到createStore()這個方法裏的第二個參數是用來「初始化狀態」的,這也許是對reducer建立初始化狀態方式的一種替代。然而,這個初始化的狀態只會被用來「state hydration」。

想象一下一個用戶刷新了你的單頁面應用,store中的狀態被重置爲reducer中的初始狀態,這樣多是不理想的。

相反,想象一個你可使用一種策略來保持store,而後在刷新的時候從新將它化合到Redux中。這就是傳送一個初始化狀態到createStore()中的緣由。

這帶來了一個有趣的概念,若是從新化合老的狀態變得這麼容易,咱們能夠將app中的狀態想象成是時間旅行。這能夠被用來進行調試或者撤銷/重作某些特性。因此將全部的狀態存儲在一個store中變得頗有意義。這就是爲何不可變的狀態可以幫助咱們的其中一個緣由。

在一次面談中,Dan Abramov 被問到「爲何你要開發Redux?」

我並非有意要建立Flux框架。當React第一次被宣佈的時候,我提議來談一談‘熱加載和時間旅行’,可是老實說,我本身也不知道該怎麼實施時間旅行

Redux with React

就像咱們已經討論過的,Redux與框架無關。在咱們開始考慮Redux跟React怎麼結合以前,明白Redux的核心概念是很是重要的。可是如今咱們已經準備好從上一篇文章中拿一個容器組件,而後將Redux應用在它上面了。

首先,這是沒有使用Redux的原始組件代碼:

import React from 'react';
import axios from 'axios';
import UserList from '../views/list-user';

const UserListContainer = React.createClass({
  getInitialState: function() {
    return {
      users: []
    };
  },

  componentDidMount: function() {
    axios.get('/path/to/user-api').then(response => {
      this.setState({users: response.data});
    });
  },

  render: function() {
    return <UserList users={this.state.users} />;
  }
});

export default UserListContainer;

ES2015 說明!這個例子已經在原始代碼的基礎上作了部分轉換,使用了ES2015的模塊功能和箭頭函數。

它所作的就是發送一個Ajax請求,而後更新它的本地狀態。可是,若是該應用的其它區域也要根據這個新獲取到的用戶列表進行改變呢,這個策略是不夠的。

有了Redux策略,咱們能夠在Ajax請求的時候分派一個action而不是進行 this.setState(),而後這個組件和其它組件能夠訂閱狀態的改變。可是事實上這帶給咱們一個問題,咱們應該怎麼設置store.subscribe()來更新組件的狀態呢?

我想我能夠提供幾個例子來手動的鏈接一些組件到Redux store。你也能夠想象一下用你的方法會怎麼作。可是最終,在這些例子的最後我會解釋有一個更好的辦法,而後忘掉這些手動的例子。而後我會介紹官方的鏈接React和Redux的模塊,叫作react-redux,因此仍是直接跳到那一步吧。

使用 react-redux 進行鏈接

爲了說明白,reactreduxreact-redux是npm上三個獨立的模塊。其中,react-redux模塊容許咱們以更方便的方式「connect」 React組件和Redux

下面給出例子:

import React from 'react';
import { connect } from 'react-redux';
import store from '../path/to/store';
import axios from 'axios';
import UserList from '../views/list-user';

const UserListContainer = React.createClass({
  componentDidMount: function() {
    axios.get('/path/to/user-api').then(response => {
      store.dispatch({
        type: 'USER_LIST_SUCCESS',
        users: response.data
      });
    });
  },

  render: function() {
    return <UserList users={this.props.users} />;
  }
});

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

export default connect(mapStateToProps)(UserListContainer);

這裏面有許多的新東西:

一、咱們從 react-redux 中引入了 connect 函數。
二、這段代碼可能從最底下的鏈接操做開始往上看會更容易理解。connect()方法實際上接收兩個參數,可是咱們這裏只顯示了一個 mapStateToProps()

connect()() 多了一個括號看起來好像很奇怪,實際上這是兩個函數的調用。首先,connect()返回了另一個函數,我想咱們能夠把這個函數賦值給一個變量名,而後調用它,可是既然在後面多加一個括號就能夠直接調用這個函數,咱們爲何還要給它設置一個函數名呢?並且,在這個函數調用結束以後,咱們根本不須要這個額外的函數名。這第二個函數須要你傳遞一個React組件。在這個例子中,傳遞的是咱們的容器組件。我敢打賭你確定正在思考「爲何要把它變得這麼複雜?」,然而,這其實是一種常見的「函數式編程」範式,因此,學習如何使用它是很是有好處的。

三、connect()第一個參數是須要返回一個對象的函數。這個對象的屬性會成爲這個組件的「props」。你能夠看到它們的狀態值。如今,我但願「mapStateToProps」變得更有意義。同時,咱們也看到mapStateToProps()這個函數接收了一個參數,這個參數就是整個Redux的store。mapStateToProps()函數的主體思想就是將這個組件須要用到的部分狀態從所有狀態中隔離出來做爲它的props屬性。

四、根據第3點中所說的,咱們將再也不須要getInitialState()的存在。同時,咱們也看到,自從users這個數組變成了props屬性而不是本地組件狀態以後,咱們參考使用this.props.users而不是this.state.users

五、Ajax的返回如今變成了一個action的分派,而不是本地狀態的更新。爲了更簡單明瞭的展現,咱們沒有使用action構造器和action type常量

下面的代碼提供了一種在用戶自定義的reducer沒有出現的時候也能夠工做的假設。注意store的userState屬性,可是這個名字是哪裏來的呢?

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

這個名字來自咱們合併全部的reducer的時候:

const reducers = combineReducers({
  userState: userReducer,
  widgetState: widgetReducer
});

userState.users屬性又是什麼?它又來自哪裏?

在這個例子中,咱們並無展現一個實際的reducer(由於它會出如今另外一個文件中),reducer決定了它所負責狀態的子屬性。爲了確保.usersuserState的一個屬性,上述例子對應的reducer可能看起來是這樣的:

const initialUserState = {
  users: []
}

const userReducer = function(state = initialUserState, action) {
  switch(action.type) {
  case 'USER_LIST_SUCCESS':
    return Object.assign({}, state, { users: action.users });
  }
  return state;
}

在 Ajax 不一樣生命週期進行分派

在咱們Ajax的例子中,咱們僅僅分派了一個action。它被特地叫作「USER_LIST_SUCCESS」,由於咱們同時也但願在Ajax調用開始的時候分派一個「USER_LIST_REQUEST」的action,在Ajax調用失敗的時候分派一個「USER_LIST_FAILED」的action。請確保讀取異步操做的文檔

分派事件

在以前的文章中,咱們看到事件應該經過容器組件傳遞到表現組件。原來 react-redux同時也能夠處理這個,一個事件只須要分派一個action:

...

const mapDispatchToProps = function(dispatch, ownProps) {
  return {
    toggleActive: function() {
      dispatch({ ... });
    }
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(UserListContainer);

在表現組件中,就像咱們以前作過的,能夠經過onClick={this.props.toggleActive}來調用事件,不須要再編寫事件自己。

容器組件省略

有時,一個容器組件只須要訂閱store,不須要任何像componentDidMount()這樣的方法來開始Ajax 請求。它只須要一個render()方法傳遞給表現組件。在這個例子中,咱們能夠像這樣構造容器組件:

import React from 'react';
import { connect } from 'react-redux';
import UserList from '../views/list-user';

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

export default connect(mapStateToProps)(UserList);

是的,父老鄉親們,這就是新的容器組件的整個文件。可是等一下,容器組件在哪裏?爲何咱們在這裏沒有用到任何的React.createClass()

事實證實,connect()方法爲咱們構造了一個容器組件。注意到這一次咱們直接傳遞的是一個表現組件,而不是咱們本身建立的容器組件。若是你真的在想容器組件幹了什麼,記住,它們的存在是爲了表現組件專心於視圖,而不是狀態。它們也傳遞狀態給子視圖做爲props。而這就是connect()實際所作的,它傳遞了狀態(做爲props)給咱們的表現組件,而後返回一個React組件來包裹這個表現組件。從本質上來講,這個包裹,就是容器組件。

因此是否是意味着上面的例子中其實有兩個容器組件包裹着一個表現組件?固然,你能夠這樣子認爲。但這並無什麼問題,只有當咱們的容器組件須要除了render()方法以外的其它方法的時候它纔是必須的。

想象這兩個容器組件是具備不一樣可是相關服務的角色:

嗯,也許這就是爲何React的logo看起來這麼像原子的緣由吧

Provider

爲了保證任何react-redux的代碼能正常工做,你須要使用一個<Provider />組件來讓你的應用知道怎樣使用react-redux。這個組件包裹了你的整個React應用。若是你正在使用 React Router,它看起來也許是這樣的:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import router from './router';

ReactDOM.render(
  <Provider store={store}>{router}</Provider>,
  document.getElementById('root')
);

經過react-redux真正「鏈接」React和Redux的東西是附加給Provider的store,這裏有個例子,關於主要入口點大概是怎麼樣

Redux with React Router

這個不作要求,可是有另外一個npm項目叫作 react-router-redux ,由於從技術上來講,路由是UI-state的一部分,並且React Router不認識Redux,因此這個項目幫助咱們鏈接這兩個東西。

你看到我作了什麼嗎?咱們走了一圈,又回到了第一篇文章!

項目最後

遵守這一系列教程,最終你能夠實現一個叫作「用戶控件」的單頁面應用。

與本系列其餘文章同樣,每一個都有相關指導文檔,在Github上也都有相關代碼指導你怎麼作。

總結

我真的但願你能喜歡我寫的這一系列文章,我意識到有許多關於React的主題咱們都沒有覆蓋到,但我試圖在保持真實的前提下,給新用戶一種跨越React基礎知識的認知,以及製做一個單頁面應用所帶來的感覺。


系列文章


翻譯文獻Leveling Up with React: Redux By Brad Westfall On March 28, 2016

翻譯做者:coocier

相關文章
相關標籤/搜索