1. 程式人生 > >深入Redux架構

深入Redux架構

redux

關於redux

之前寫了一篇通過一個demo了解Redux,但對於redux的核心方法沒有進行深入剖析,在此重新總結學習,完整的代碼看這裏。(參考了React 技術棧系列教程)

技術分享

什麽情況需要用redux?

  • 用戶的使用方式復雜

  • 不同身份的用戶有不同的使用方式(比如普通用戶和管理員)

  • 多個用戶之間可以協作

  • 與服務器大量交互,或者使用了WebSocket

  • View要從多個來源獲取數據

簡單說,如果你的UI層非常簡單,沒有很多互動,Redux 就是不必要的,用了反而增加復雜性。多交互、多數據源場景就比較適合使用Redux。

設計思想:

  • Web 應用是一個狀態機,視圖與狀態是一一對應的。

  • 所有的狀態,保存在一個對象裏面。

Redux工作流程:

技術分享

首先,用戶發出 Action。

store.dispatch(action);

然後,Store 自動調用 Reducer,並且傳入兩個參數:當前 State 和收到的 Action。 Reducer 會返回新的 State 。

let nextState = todoApp(previousState, action);

State 一旦有變化,Store 就會調用監聽函數。

// 設置監聽函數store.subscribe(listener);

listener可以通過store.getState()得到當前狀態。如果使用的是 React,這時可以觸發重新渲染 View。

function listerner() {
  let newState = store.getState();
  component.setState(newState);   
}

如果現在沒理解以上流程,不要急,看完以下API就差不多能懂得Redux的核心機制了。

回到頂部(go to top)

API

Store

Store 就是保存數據的地方,你可以把它看成一個容器。整個應用只能有一個 Store。

Redux 提供createStore這個函數,用來生成 Store。

下面代碼中,createStore函數接受另一個函數作為參數,返回新生成的 Store 對象。

import { createStore } from ‘redux‘;
const store = createStore(fn);

State

Store對象包含所有數據。如果想得到某個時點的數據,就要對 Store 生成快照。這種時點的數據集合,就叫做 State。

當前時刻的 State,可以通過store.getState()拿到。

import { createStore } from ‘redux‘;
const store = createStore(fn);

const state = store.getState();

Redux 規定, 一個 State 對應一個 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什麽樣,反之亦然。

Action

State 的變化,會導致 View 的變化。但是,用戶接觸不到 State,只能接觸到 View。所以,State 的變化必須是 View 導致的。Action 就是 View 發出的通知,表示 State 應該要發生變化了。

Action 是一個對象。其中的type屬性是必須的,表示 Action 的名稱。其他屬性可以自由設置,社區有一個規範可以參考。

const action = {
  type: ‘ADD_TODO‘,
  payload: ‘Learn Redux‘};

上面代碼中,Action 的名稱是ADD_TODO,它攜帶的信息是字符串Learn Redux。

可以這樣理解,Action 描述當前發生的事情。改變 State 的唯一辦法,就是使用 Action。它會運送數據到 Store。

Action Creator

View 要發送多少種消息,就會有多少種 Action。如果都手寫,會很麻煩。可以定義一個函數來生成 Action,這個函數就叫 Action Creator。

技術分享

const ADD_TODO = ‘添加 TODO‘;function addTodo(text) {  return {
    type: ADD_TODO,
    text
  }
}

const action = addTodo(‘Learn Redux‘);

技術分享

store.dispatch()

store.dispatch()是 View 發出 Action 的唯一方法。

技術分享

import { createStore } from ‘redux‘;
const store = createStore(fn);

store.dispatch({
  type: ‘ADD_TODO‘,
  payload: ‘Learn Redux‘});

技術分享

上面代碼中,store.dispatch接受一個 Action 對象作為參數,將它發送出去。

結合 Action Creator,這段代碼可以改寫如下。

store.dispatch(addTodo(‘Learn Redux‘));

Reducer

Store 收到 Action 以後,必須給出一個新的 State,這樣 View 才會發生變化。這種 State 的計算過程就叫做 Reducer。

Reducer 是一個函數,它接受 Action 和當前 State 作為參數,返回一個新的 State。下面是一個實際的例子

技術分享

const defaultState = 0;
const reducer = (state = defaultState, action) => {  switch (action.type) {    case ‘ADD‘:      return state + action.payload;    default: 
      return state;
  }
};

const state = reducer(1, {
  type: ‘ADD‘,
  payload: 2});

技術分享

上面代碼中,reducer函數收到名為ADD的 Action 以後,就返回一個新的 State,作為加法的計算結果。其他運算的邏輯(比如減法),也可以根據 Action 的不同來實現。

實際應用中,Reducer 函數不用像上面這樣手動調用,store.dispatch方法會觸發 Reducer 的自動執行。為此,Store 需要知道 Reducer 函數,做法就是在生成 Store 的時候,將 Reducer 傳入createStore方法。

import { createStore } from ‘redux‘;
const store = createStore(reducer);

上面代碼中,createStore接受 Reducer 作為參數,生成一個新的 Store。以後每當store.dispatch發送過來一個新的 Action,就會自動調用 Reducer,得到新的 State。

store.subscribe()

Store 允許使用store.subscribe方法設置監聽函數,一旦 State 發生變化,就自動執行這個函數。

import { createStore } from ‘redux‘;
const store = createStore(reducer);

store.subscribe(listener);

顯然,只要把 View 的更新函數(對於 React 項目,就是組件的render方法或setState方法)放入listen,就會實現 View 的自動渲染。

store.subscribe方法返回一個函數,調用這個函數就可以解除監聽。

let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

unsubscribe();

回到頂部(go to top)

中間件與異步操作

一個關鍵問題沒有解決:異步操作怎麽辦?Action 發出以後,Reducer 立即算出 State,這叫做同步;Action 發出以後,過一段時間再執行 Reducer,這就是異步。

怎麽才能 Reducer 在異步操作結束後自動執行呢?這就要用到新的工具:中間件(middleware)。

技術分享

為了理解中間件,讓我們站在框架作者的角度思考問題:如果要添加功能,你會在哪個環節添加?

(1)Reducer:純函數,只承擔計算 State 的功能,不合適承擔其他功能,也承擔不了,因為理論上,純函數不能進行讀寫操作。

(2)View:與 State 一一對應,可以看作 State 的視覺層,也不合適承擔其他功能。

(3)Action:存放數據的對象,即消息的載體,只能被別人操作,自己不能進行任何操作。

想來想去,只有發送 Action 的這個步驟,即store.dispatch()方法,可以添加功能。

中間件的用法

本文不涉及如何編寫中間件,因為常用的中間件都有現成的,只要引用別人寫好的模塊即可。比如,上一節的日誌中間件,就有現成的redux-logger模塊。這裏只介紹怎麽使用中間件。

技術分享

import { applyMiddleware, createStore } from ‘redux‘;
import createLogger from ‘redux-logger‘;
const logger = createLogger();

const store = createStore(
  reducer,
  applyMiddleware(logger)
);

技術分享

上面代碼中,redux-logger提供一個生成器createLogger,可以生成日誌中間件logger。然後,將它放在applyMiddleware方法之中,傳入createStore方法,就完成了store.dispatch()的功能增強。

這裏有兩點需要註意:

(1)createStore方法可以接受整個應用的初始狀態作為參數,那樣的話,applyMiddleware就是第三個參數了。

const store = createStore(
  reducer,
  initial_state,
  applyMiddleware(logger)
);

(2)中間件的次序有講究。

const store = createStore(
  reducer,
  applyMiddleware(thunk, promise, logger)
);

上面代碼中,applyMiddleware方法的三個參數,就是三個中間件。有的中間件有次序要求,使用前要查一下文檔。比如,logger就一定要放在最後,否則輸出結果會不正確。

回到頂部(go to top)

異步操作的基本思路

理解了中間件以後,就可以處理異步操作了。

同步操作只要發出一種 Action 即可,異步操作的差別是它要發出三種 Action。

  • 操作發起時的 Action

  • 操作成功時的 Action

  • 操作失敗時的 Action

以向服務器取出數據為例,三種 Action 可以有兩種不同的寫法。

技術分享

// 寫法一:名稱相同,參數不同{ type: ‘FETCH_POSTS‘ }
{ type: ‘FETCH_POSTS‘, status: ‘error‘, error: ‘Oops‘ }
{ type: ‘FETCH_POSTS‘, status: ‘success‘, response: { ... } }// 寫法二:名稱不同{ type: ‘FETCH_POSTS_REQUEST‘ }
{ type: ‘FETCH_POSTS_FAILURE‘, error: ‘Oops‘ }
{ type: ‘FETCH_POSTS_SUCCESS‘, response: { ... } }

技術分享

除了 Action 種類不同,異步操作的 State 也要進行改造,反映不同的操作狀態。下面是 State 的一個例子。

技術分享

let state = {  // ... 
  isFetching: true,
  didInvalidate: true,
  lastUpdated: ‘xxxxxxx‘};

技術分享

上面代碼中,State 的屬性isFetching表示是否在抓取數據。didInvalidate表示數據是否過時,lastUpdated表示上一次更新時間。

現在,整個異步操作的思路就很清楚了。

  • 操作開始時,送出一個 Action,觸發 State 更新為"正在操作"狀態,View 重新渲染

  • 操作結束後,再送出一個 Action,觸發 State 更新為"操作結束"狀態,View 再一次重新渲染

redux-thunk中間件

異步操作至少要送出兩個 Action:用戶觸發第一個 Action,這個跟同步操作一樣,沒有問題;如何才能在操作結束時,系統自動送出第二個 Action 呢?

奧妙就在 Action Creator 之中。

技術分享

class AsyncApp extends Component {
  componentDidMount() {
    const { dispatch, selectedPost } = this.props
    dispatch(fetchPosts(selectedPost))
  }// ...

技術分享

上面代碼是一個異步組件的例子。加載成功後(componentDidMount方法),它送出了(dispatch方法)一個 Action,向服務器要求數據 fetchPosts(selectedSubreddit)。這裏的fetchPosts就是 Action Creator。

下面就是fetchPosts的代碼,關鍵之處就在裏面。

技術分享

技術分享

const fetchPosts = postTitle => (dispatch, getState) => {
  dispatch(requestPosts(postTitle));  return fetch(`/some/API/${postTitle}.json`)
    .then(response => response.json())
    .then(json => dispatch(receivePosts(postTitle, json)));
  };
};// 使用方法一store.dispatch(fetchPosts(‘reactjs‘));// 使用方法二store.dispatch(fetchPosts(‘reactjs‘)).then(() =>
  console.log(store.getState())
);

技術分享

上面代碼中,fetchPosts是一個Action Creator(動作生成器),返回一個函數。這個函數執行後,先發出一個Action(requestPosts(postTitle)),然後進行異步操作。拿到結果後,先將結果轉成 JSON 格式,然後再發出一個 Action( receivePosts(postTitle, json))。

上面代碼中,有幾個地方需要註意。

(1)fetchPosts返回了一個函數,而普通的 Action Creator 默認返回一個對象。

(2)返回的函數的參數是dispatch和getState這兩個 Redux 方法,普通的 Action Creator 的參數是 Action 的內容。

(3)在返回的函數之中,先發出一個 Action(requestPosts(postTitle)),表示操作開始。

(4)異步操作結束之後,再發出一個 Action(receivePosts(postTitle, json)),表示操作結束。

這樣的處理,就解決了自動發送第二個 Action 的問題。但是,又帶來了一個新的問題,Action 是由store.dispatch方法發送的。而store.dispatch方法正常情況下,參數只能是對象,不能是函數。

這時,就要使用中間件redux-thunk。

技術分享

import { createStore, applyMiddleware } from ‘redux‘;
import thunk from ‘redux-thunk‘;
import reducer from ‘./reducers‘;// Note: this API requires redux@>=3.1.0const store = createStore(
  reducer,
  applyMiddleware(thunk)
);

技術分享

上面代碼使用redux-thunk中間件,改造store.dispatch,使得後者可以接受函數作為參數。

因此,異步操作的第一種解決方案就是,寫出一個返回函數的 Action Creator,然後使用redux-thunk中間件改造store.dispatch。

回到頂部(go to top)

React-Redux的用法

為了方便使用,Redux 的作者封裝了一個 React 專用的庫 React-Redux,本文主要介紹它。

這個庫是可以選用的。實際項目中,你應該權衡一下,是直接使用 Redux,還是使用 React-Redux。後者雖然提供了便利,但是需要掌握額外的 API,並且要遵守它的組件拆分規範。

技術分享

React-Redux 將所有組件分成兩大類:UI 組件(presentational component)和容器組件(container component)。

UI組件

UI 組件有以下幾個特征。

  • 只負責 UI 的呈現,不帶有任何業務邏輯

  • 沒有狀態(即不使用this.state這個變量)

  • 所有數據都由參數(this.props)提供

  • 不使用任何 Redux 的 API

下面就是一個 UI 組件的例子。

const Title =
  value => <h1>{value}</h1>;

因為不含有狀態,UI 組件又稱為"純組件",即它純函數一樣,純粹由參數決定它的值。

容器組件

容器組件的特征恰恰相反。

  • 負責管理數據和業務邏輯,不負責 UI 的呈現

  • 帶有內部狀態

  • 使用 Redux 的 API

總之,只要記住一句話就可以了:UI 組件負責 UI 的呈現,容器組件負責管理數據和邏輯。

你可能會問,如果一個組件既有 UI 又有業務邏輯,那怎麽辦?回答是,將它拆分成下面的結構:外面是一個容器組件,裏面包了一個UI 組件。前者負責與外部的通信,將數據傳給後者,由後者渲染出視圖。

React-Redux 規定,所有的 UI 組件都由用戶提供,容器組件則是由 React-Redux 自動生成。也就是說,用戶負責視覺層,狀態管理則是全部交給它。

connect()

React-Redux 提供connect方法,用於從 UI 組件生成容器組件。connect的意思,就是將這兩種組件連起來。

connect方法的完整 API 如下。

技術分享

import { connect } from ‘react-redux‘const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

技術分享

上面代碼中,TodoList是 UI 組件,VisibleTodoList就是由 React-Redux 通過connect方法自動生成的容器組件。connect方法接受兩個參數:mapStateToProps和mapDispatchToProps。它們定義了 UI 組件的業務邏輯。前者負責輸入邏輯,即將state映射到 UI 組件的參數(props),後者負責輸出邏輯,即將用戶對 UI 組件的操作映射成 Action。

mapStateToProps

mapStateToProps是一個函數。它的作用就是像它的名字那樣,建立一個從(外部的)state對象到(UI 組件的)props對象的映射關系。

作為函數,mapStateToProps執行後應該返回一個對象,裏面的每一個鍵值對就是一個映射。請看下面的例子。

const mapStateToProps = (state) => {  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

上面代碼中,mapStateToProps是一個函數,它接受state作為參數,返回一個對象。這個對象有一個todos屬性,代表 UI 組件的同名參數,後面的getVisibleTodos也是一個函數,可以從state算出 todos 的值。

下面就是getVisibleTodos的一個例子,用來算出todos。

技術分享

const getVisibleTodos = (todos, filter) => {  switch (filter) {    case ‘SHOW_ALL‘:      return todos    case ‘SHOW_COMPLETED‘:      return todos.filter(t => t.completed)    case ‘SHOW_ACTIVE‘:      return todos.filter(t => !t.completed)    default:      throw new Error(‘Unknown filter: ‘ + filter)
  }
}

技術分享

mapStateToProps會訂閱 Store,每當state更新的時候,就會自動執行,重新計算 UI 組件的參數,從而觸發 UI 組件的重新渲染。

mapStateToProps的第一個參數總是state對象,還可以使用第二個參數,代表容器組件的props對象。

技術分享

// 容器組件的代碼//    <FilterLink filter="SHOW_ALL">//      All//    </FilterLink>const mapStateToProps = (state, ownProps) => {  return {
    active: ownProps.filter === state.visibilityFilter
  }
}

技術分享

使用ownProps作為參數後,如果容器組件的參數發生變化,也會引發 UI 組件重新渲染。

connect方法可以省略mapStateToProps參數,那樣的話,UI 組件就不會訂閱Store,就是說 Store 的更新不會引起 UI 組件的更新。

mapDispatchToProps()

mapDispatchToProps是connect函數的第二個參數,用來建立 UI 組件的參數到store.dispatch方法的映射。也就是說,它定義了哪些用戶的操作應該當作 Action,傳給 Store。它可以是一個函數,也可以是一個對象。

如果mapDispatchToProps是一個函數,會得到dispatch和ownProps(容器組件的props對象)兩個參數。

技術分享

const mapDispatchToProps = (
  dispatch,
  ownProps
) => {  return {
    onClick: () => {
      dispatch({
        type: ‘SET_VISIBILITY_FILTER‘,
        filter: ownProps.filter
      });
    }
  };
}

技術分享

從上面代碼可以看到,mapDispatchToProps作為函數,應該返回一個對象,該對象的每個鍵值對都是一個映射,定義了 UI 組件的參數怎樣發出 Action。

如果mapDispatchToProps是一個對象,它的每個鍵名也是對應 UI 組件的同名參數,鍵值應該是一個函數,會被當作 Action creator ,返回的 Action 會由 Redux 自動發出。舉例來說,上面的mapDispatchToProps寫成對象就是下面這樣。

技術分享

const mapDispatchToProps = {
  onClick: (filter) => {
    type: ‘SET_VISIBILITY_FILTER‘,
    filter: filter
  };
}

技術分享

<Provider>組件

connect方法生成容器組件以後,需要讓容器組件拿到state對象,才能生成 UI 組件的參數。React-Redux 提供Provider組件,可以讓容器組件拿到state。

技術分享

import { Provider } from ‘react-redux‘import { createStore } from ‘redux‘import todoApp from ‘./reducers‘import App from ‘./components/App‘let store = createStore(todoApp);

render(  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById(‘root‘)
)

技術分享

上面代碼中,Provider在根組件外面包了一層,這樣一來,App的所有子組件就默認都可以拿到state了。

React-Router路由庫

使用React-Router的項目,與其他項目沒有不同之處,也是使用Provider在Router外面包一層,畢竟Provider的唯一功能就是傳入store對象。

技術分享

const Root = ({ store }) => (  <Provider store={store}>
    <Router>
      <Route path="/" component={App} />
    </Router>
  </Provider>
);

技術分享


深入Redux架構