[譯] Redux 的工做過程

Redux 的工做過程: 一個計數器例子

在學習了一些 React 後開始學習 Redux,Redux 的工做過程讓人感到很困惑。javascript

Actions,reducers,action creators(Action 建立函數),middleware(中間件),pure functions(純函數),immutability(不變性)…前端

這些術語看起來很是陌生。java

因此在這篇文章中我將用一種有利於你們理解的反向剖析的方法去揭開 Redux 怎樣工做的神祕面紗。在 上一篇 中,在提出專業術語以前我將嘗試用簡單易懂的語言去解釋 Redux。react

若是你還不明確 Redux 是幹什麼的 或者爲何要使用它,請先移步 這篇文章 而後再回到這裏繼續閱讀。android

第一:明白 React 的狀態 state

咱們將從一個簡單的使用 React 狀態的例子開始,而後一點一點地添加Redux。ios

這是一個計數器:git

計數器組件

這裏是代碼 (爲了使代碼簡單我沒有貼出 CSS 代碼,因此下面代碼的效果會不會像上面圖片同樣美觀):github

import React from 'react';

class Counter extends React.Component {
  state = { count: 0 }

  increment = () => {
    this.setState({
      count: this.state.count + 1
    });
  }

  decrement = () => {
    this.setState({
      count: this.state.count - 1
    });
  }

  render() {
    return (
      <div>
        <h2>Counter</h2>
        <div>
          <button onClick={this.decrement}>-</button>
          <span>{this.state.count}</span>
          <button onClick={this.increment}>+</button>
        </div>
      </div>
    )
  }
}

export default Counter;
複製代碼

簡單的看一下他是怎樣跑起來的:npm

  • 這個 count 狀態被存儲在最外層組件 Counter 裏面
  • 當用戶點擊 「+」,這個按鈕的 onClick 回調函數被觸發, 也就是組件 Counter 裏面的 increment 方法被調用。
  • increment 方法用新的數字更新狀態 count。
  • 因爲狀態被改變了, React 從新渲染 Counter 組件 (還有它的子組件), 而後顯示新的計數器的值.

若是你想要了解更多的狀態怎麼被改變的細節,去閱讀 React 中狀態的圖形化指南 而後再回到這裏。嚴格來說:若是上面的例子沒有幫助你回顧起 React 的 state ,那麼在你學習 Redux 以前應該去學習 React 的 state 是怎麼工做的。編程

快速開始

若是你想經過代碼學習,如今就建立一個項目:

  • 若是你以前沒有安裝 create-react-app ,那麼先安裝 (npm install -g create-react-app)
  • 建立一個項目: create-react-app redux-intro
  • 打開 src/index.js 而後用下面的代碼進行替換:
import React from 'react';
import { render } from 'react-dom';
import Counter from './Counter';

const App = () => (
  <div>
    <Counter />
  </div>
);

render(<App />, document.getElementById('root'));
複製代碼
  • 用上面的計數器代碼建立一個 src/Counter.js

如今: 添加 Redux

第一部分中討論到,Redux 保存應用程序的狀態 state 在單一的狀態樹 store中。而後你能夠將 state 的部分抽離出來,而後以 props 的方式傳入組件。這使你能夠把數據保存在一個全局的位置(狀態樹 store )而後將其注入到應用程序中的任何一個組件中,而不用經過多層級的屬性傳遞。

注意:你可能常常看到 「state」 和 「store」 混着使用,可是嚴格來說: state是數據,而 store 是數據保存的地方。

咱們接着往下走,利用你的編輯器繼續編輯咱們下面的代碼,它將幫助你理解 Redux 怎麼工做(咱們經過講解一些錯誤來繼續)。

添加 Redux 到你的項目中:

$ yarn add redux react-redux
複製代碼

redux vs react-redux

等等 — 這是兩個庫嗎?你可能會問 「react-redux 是什麼」?對不起,我一直在騙你。

你看,redux 給了你一個狀態樹 store,讓你能夠把狀態 state 存在裏面,而後能夠把狀態取出來,當狀態改變的時候能夠作出響應。然而這是他它作的全部事。實際上正是 react-redux 將 state 與 React 組件聯繫起來。實際上:redux 和 React 一點兒也沒有關係。

這些庫就像豌豆莢裏面的兩粒豌豆,99.999% 的時候當有人在 React 的背景下提到 「Redux」 的時候,他們指的是這兩個庫。因此記住:當你在 StackOverflow 或者 Reddit 或者其它任何地方看到 Redux 時,他指的是這兩個庫。

最後一件事

大多數教程一開始就建立一個 store 狀態樹,設置 Redux,寫一個 reducer,等等,出如今屏幕上的任何效果在展示出來以前都會通過大量的操做。

我將採用一種反向推導的方法,使用一樣多的代碼展示出一樣的效果。可是但願每個步驟後面的原理都能展示地更加清楚。

回到計數器的應用程序,咱們把組件的狀態轉移到 Redux。

咱們把狀態從組件裏面移除,由於咱們很快能夠從 Redux 中獲取它們:

import React from 'react';

class Counter extends React.Component {
  increment = () => {
    // 後面填充
  }

  decrement = () => {
    // 後面填充
  }

  render() {
    return (
      <div>
        <h2>Counter</h2>
        <div>
          <button onClick={this.decrement}>-</button>
          <span>{this.props.count}</span>
          <button onClick={this.increment}>+</button>
        </div>
      </div>
    )
  }
}

export default Counter;
複製代碼

計數器的流程

咱們注意到 {this.state.count} 改變成了 {this.props.count}。固然這不會起做用,由於計數器組件尚未接受 count 屬性,咱們經過 Redux 注入這個屬性。

爲了從 Redux 中得到狀態 count,咱們須要在模塊的頂部導入 connect 方法:

import { connect } from 'react-redux';
複製代碼

而後接下來咱們須要 「connect」 計數器組件到 Redux 中:

// 添加這個函數:
function mapStateToProps(state) {
  return {
    count: state.count
  };
}

// 而後這樣替換:
// 默認導出計數器組件;

// 這樣導出:
export default connect(mapStateToProps)(Counter);
複製代碼

這將發生錯誤 (在第二部分會有更多錯誤)。

之前咱們導出函數自己,如今咱們把它用 connect 函數包裝後調用。

什麼是 connect

你可能注意到這個函數調用看起來有一些奇怪。爲何是 connect(mapStateToProps)(Counter) 而不是 connect(mapStateToProps, Counter) 或者 connect(Counter, mapStateToProps)?這將發生什麼呢?

之因此這樣寫是由於 connect 是一個高階函數,當你調用它的時候會返回一個函數,而後用一個組件作參數調用那個函數返回一個新的包裝過的組件。

返回的組件另外一個名字叫作高階組件 (又叫作 「HOC」)。高階組件被指責有不少的缺點,可是他們仍然很是有用,connect 就是一個很好的例子。

connect 鏈接整個狀態到了Redux,經過你本身提供的 mapStateToProps 函數, 這須要一個自定義的函數由於只有你本身知道狀態在 Redux 中的模型。

connect 鏈接了全部的狀態,「嘿,告訴我你須要從混亂的狀態中獲得什麼」。

mapStateToProps 函數中返回的狀態做爲屬性注入到你的組件中。上面例子中的 state.count 做爲 count 屬性:對象中的鍵名做爲屬性名,它們對應的值做爲屬性的值。因此你看,從函數的字面意思上是定義了狀態到屬性的映射

錯誤意味着有進展!

代碼進行到這裏,你會在控制檯裏面看到下面的錯誤:

Could not find 「store」 in either the context or props of 「Connect(Counter)」. Either wrap the root component in a , or explicitly pass "store" as a prop to "Connect(Counter)".

由於 connect 從 Redux store 樹裏面獲取狀態,而咱們尚未建立狀態樹或者說告訴 app 怎樣去找到 store 樹,這是一個合乎邏輯的錯誤,Redux 還不知道如今發生了什麼事。

提供一個狀態樹 store

Redux 控制着整個 app 的所有狀態,經過 react-redux 裏面的 Provider 組件包裹着整個 app,app 裏面的每個組件均可以經過 connect 去進入到 Redux store 裏面獲取狀態。

這意味着最外圍的 App 組件,以及 App 的子組件(像 Counter),甚至他們子組件的子組件等等,全部的組件均可以訪問狀態樹 store,只要把他們經過 connect 函數調用。

我不是說要把每個組件都用 connect 函數調用,那是一個很糟糕的作法(設計混亂並且太慢了)。

Provider 看起來很具備魔性,實際上在掛載的時候使用了 React 的 「context」 特性。

Provider 就像一個祕密通道鏈接到了每個組件,使用 connect 打開了通向每個組件的大門。

想象一下,把糖漿倒在一堆煎餅上,假如你只把糖漿倒在了最上面的煎餅上,怎麼才能讓全部的煎餅都能蘸到糖漿呢。 Provider 爲 Redux 作了這件事。

在文件 src/index.js中,導入 Provider 組件而且用它來包裹 App 組件的內容。

import { Provider } from 'react-redux';
...

const App = () => (
  <Provider>
    <Counter/>
  </Provider>
);
複製代碼

咱們仍然會遇到報錯,由於 Provider 須要一個 store 狀態樹才能起做用,它會把 store 做爲屬性,因此咱們首先須要建立一個 store。

建立一個 store

Redux 使用一個方便的函數來建立 stores,這個函數就是 createStore。好了,如今讓咱們來建立一個 store 而後把它做爲屬性傳入 Provider 組件:

import { createStore } from 'redux';

const store = createStore();

const App = () => (
  <Provider store={store}>
    <Counter/>
  </Provider>
);
複製代碼

又產生了另一個不一樣的錯誤:

Expected the reducer to be a function.

如今是 Redux 的問題了,Redux 不是那麼的智能,你可能但願建立一個 store,它就會從 store 中 給你一箇中很好的默認的值,哪怕是一個空對象?

可是毫不會這樣,Redux 不會對你的狀態的組成作出任何的猜想,狀態的組成結構徹底取決於你本身。他能夠是一個對象, 一個數字, 一個字符串, 或者是你須要的任何形式。因此咱們必須提供一個函數去返回這個狀態,這個函數就叫作reducer(後面會解釋爲何這麼命名)。讓咱們來看看函數最簡單的狀況,將它做爲函數 createStore 的參數,看看會發生什麼:

function reducer() {
  // just gonna leave this blank for now
  // which is the same as `return undefined;`
}

const store = createStore(reducer);
複製代碼

Reducer 必需要有返回值

又產生了另外的錯誤:

Cannot read property ‘count’ of undefined

產生這個錯誤是由於咱們試圖去取得 state.count,可是 state 卻沒有定義。Redux 但願 reducer 函數爲 state 返回一個值,而不是返回一個 undefined

reducer 函數應該返回一個狀態,實際上它應該用利用當前狀態去返回新的狀態

讓咱們用 reducer 函數去返回知足咱們須要的狀態形式:一個含有 count 屬性的對象。

function reducer() {
  return {
    count: 42
  };
}
複製代碼

嘿!這個 count 如今顯示爲 「42」,神奇吧。

只是有一個問題:count 一直顯示爲42。

目前爲止

在咱們進一步瞭解怎麼更新計數器的值以前,咱們先來了解一下到目前爲止咱們作了些什麼:

  • 咱們寫了一個 mapStateToProps 函數,該函數的做用是:把 Redux 中的狀態轉換成一個包含屬性的對象。
  • 咱們用模塊 react-redux 中的函數 connect 把 Redux store 狀態樹和 Counter 組件鏈接起來,使用 mapStateToProps 函數配置了怎麼聯繫。
  • 咱們建立了一個 reducer 函數去告訴 Redux 咱們的狀態應該是什麼形式的。
  • 咱們使用 reducercreateStore 函數的參數,用它建立了一個 store。
  • 咱們把整個組件包裹在了 react-redux 中的組件 Provider 中,向該組件傳入了 store 做爲屬性。
  • 這個程序工做的很好,惟一的問題是計數器顯示停留在了42。

你跟着我作到如今了嗎?

互動起來 (讓計數器工做)

我知道到目前爲止咱們的程序是不好勁的,大家已經寫了一個顯示着數字 「42」 和兩個無效的按鈕的靜態的 HTML 頁面,不過你還在繼續閱讀,接下來將繼續用 React 和 Redux 和其它的一些東西讓咱們的程序變得複雜起來。

我保證接下來作的事情會讓上面作的一切都值得。

事實上,我收回剛纔那句話,一個簡單的計數器的例子是一個很好的教學例子,可是 Redux 讓應用變得複雜了,React 的 state 應用起來其實也很簡單,甚至通常的 JS 代碼也可以實現的很好,挑選正確的工具作正確的事,Redux 不老是那個合適的工具,不過我偏題了。

初始化狀態

咱們須要一個方式去告訴 Redux 改變計數器的值。

還記得咱們寫的 reducer 函數嗎?(固然你確定記得,由於那是兩分鐘以前的事)。

還記得我說過它會使用當前狀態返回新的狀態嗎?好的,我再重複一次,實際上,它使用當前狀態和一個 action 做爲參數,而後返回一個新的狀態,咱們應該這樣寫:

function reducer(state, action) {
  return {
    count: 42
  };
}
複製代碼

Redux 第一次調用這個函數的時候會以 undefined 做爲實參替代 state,意味着返回的是初始狀態,對於咱們來講,可能返回的是一個屬性 count 值爲 0 的對象。

在 reducer 上面寫初始狀態是很常見的,當 state 參數未定義的時候,使用 ES6 的默認參數的特性爲 state 參數提供一個參數。

const initialState = {
  count: 0
};

function reducer(state = initialState, action) {
  return state;
}
複製代碼

這樣子試試呢,代碼仍然會起做用,不過如今計數器停留在了 0 而不是 42,多麼讓人驚訝。

Action

咱們最後談談 action 參數,這是什麼呢?它來自哪裏呢? 咱們怎麼用它去改變不變的 counter 呢?

一個 「action」 是一個描述了咱們想要改變什麼的 JS 對象,爲一個要求就是對象必需要有一個 type 屬性,它的值應該是一個字符串,這裏有一個例子:

{
  type: "INCREMENT"
}
複製代碼

這是另一個例子:

{
  type: "DECREMENT"
}
複製代碼

你的大腦在快速運轉嗎?你知道接下來咱們要作什麼嗎?

對 Actions 作出響應

還記得 reducer 的做用是用當前狀態和一個action去計算出新的狀態吧。因此若是一個 reducer 接受了一個 action 例如 { type: "INCREMENT" },你想要返回什麼做爲新的狀態呢?

若是你像下面這樣想,那麼你就想對了:

function reducer(state = initialState, action) {
  if(action.type === "INCREMENT") {
    return {
      count: state.count + 1
    };
  }

  return state;
}
複製代碼

使用 switch 語句和 case 語句處理每個 action 是很常見的寫法把你的 reducer 函數寫成下面這樣子:

function reducer(state = initialState, action) {
  switch(action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1
      };
    case 'DECREMENT':
      return {
        count: state.count - 1
      };
    default:
      return state;
  }
}
複製代碼

老是返回一個狀態

你會注意到函數默認返回的是 return state。這很重要,由於 action 不知道要作什麼,Redux 經過 action 去調用你的 reducer 函數。實際上 你接受的第一個 action 是 { type: "@@redux/INIT" }。試着在 switch 前面寫一個 console.log(action) 看看會打印出什麼。

還記得 reducer 的工做是返回一個新狀態吧,即便當前狀態沒有發生改變也要返回。 你不想從 「有一個狀態」 變成 「state = undefined」 吧? 在你忘了 default 狀況的時候就會發生這樣的事,不要這樣作。

永遠不要改變狀態

永遠不要去作這件事:不要改變 state。State 是不可變的。你不能夠改變它,意味着你不能這樣作:

function brokenReducer(state = initialState, action) {
  switch(action.type) {
    case 'INCREMENT':
      // 不,不要這樣作,這樣正在改變狀態
      state.count++;
      return state;

    case 'DECREMENT':
      // 不要這樣作,這也是在改變狀態
      state.count--;
      return state;

    default:
      // 這樣作是很好的.
      return state;
  }
}
複製代碼

你也不要作這樣的事,好比寫 state.foo = 7 或者 state.items.push(newItem),或者 delete state.something

把這想象爲一場遊戲,你惟一能作的事就是 return { ... },這是一個有趣的遊戲,一開始遊戲有些讓人抓狂,可是隨着你的練習你會以爲遊戲愈來愈有意思。

我編寫了一個簡短的指南關於怎麼去處理不可變的更新,展現了七種常見的包括對象和數組在內的更新模式。

全部的規則…

老是返回一個狀態,不要去改變狀態,不要鏈接到每個組件,吃你本身的西藍花,不要在外面待着超過 11 點...,真累啊。這就像一個規則工廠,我甚至不知道那是什麼。

是的,Redux 可能就像一個霸道的父母。可是都是出於愛。來自函數式編程的愛。

Redux 創建在不變性的基礎上,由於改變全局的狀態就是一條通向毀滅的道路。

你是否使用一個全局對象去保存整個 app 的狀態?一開始運行的很好,很容易,而後狀態在沒有任何預測的狀況下發生了改變,並且幾乎不可能去找到改變狀態的代碼。

Redux 使用一些簡單的規則去避免了這樣的問題,State 是隻讀的,actions 是惟一修改狀態的方式,改變狀態只有一種方式:這個方式就是:action -> reducer -> 新的狀態。reducer 必須是一個純函數,它不能修改它的參數。

有插件能夠幫助你去記錄每個 action,追溯它們,你能夠想象到的一切。從時間上追溯調試是建立 Redux 的動機之一。

Actions 來自哪裏呢?

讓人迷惑的一部分仍然存在:咱們須要一個方式去讓一個 action 進入到咱們的 reducer 中,咱們才能增長或者減小這個計數器。

Action 不是被生成的,它們是被dispatched的,有一個小巧的函數叫作dispatch。

dispatch 函數由 Redux store 的實例提供,也就是說,你不能夠僅僅經過 import { dispatch }得到 dispatch 函數。你能夠調用 store.dispatch(someAction),可是那不是很方便,由於 store 的實例只在一個文件裏面能夠被得到。

很幸運,咱們還有 connect 函數。除了注入 mapStateToProps 函數的返回值做爲屬性之外,connect 函數dispatch 函數做爲屬性注入了組件,使用這麼一點知識,咱們又可讓計數器工做起來了。

這裏是最後的組件形式,若是你一直跟着寫到了這裏,那麼惟一要改變的實現就是 incrementdecrement:它們如今能夠調用 dispatch 屬性,經過它分發一個 action。

import React from 'react';
import { connect } from 'react-redux';

class Counter extends React.Component {
  increment = () => {
    this.props.dispatch({ type: 'INCREMENT' });
  }

  decrement = () => {
    this.props.dispatch({ type: 'DECREMENT' });
  }

  render() {
    return (
      <div>
        <h2>Counter</h2>
        <div>
          <button onClick={this.decrement}>-</button>
          <span>{this.props.count}</span>
          <button onClick={this.increment}>+</button>
        </div>
      </div>
    )
  }
}

function mapStateToProps(state) {
  return {
    count: state.count
  };
}

export default connect(mapStateToProps)(Counter);
複製代碼

整個項目的代碼(它的兩個文件)能夠在 Github上面找到。

如今怎樣了呢?

利用 Counter 程序做爲一個傳送帶,你能夠繼續學習會更多的 Redux 知識了。

「什麼?! 還有更多?!」

還有不少的地方我沒有講到,我但願這個介紹是容易理解的 – action constants, action 建立函數, 中間件, thunks 和異步調用, selectors, 等等。 還有不少。這個 Redux docs 文檔寫的很好,覆蓋了我講到的全部知識和更多的知識。

你已經瞭解到了基本的思想,但願你理解了數據怎麼 Redux 裏面變化 (dispatch(action) -> reducer -> new state -> re-render),reducer 作了什麼,action 又作了什麼,它們是怎麼做用在一塊兒的。

我將會發佈一個新的課程,課程涵蓋到全部的這些東西和更多的知識!這裏登陸 去關注.

以按部就班的方式學習 React,查看個人 - 免費查看兩個示例章節。

就我而言,即便是免費的介紹也是值得的。 — Isaac


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索