Redux入門學習系列教程(二)

Redux入門學習系列教程(一)
Redux入門學習系列教程(二)
Redux入門學習系列教程(三)
Redux入門學習系列教程(四)

作者結(jié)合文檔,給出入門Redux學習 Demo示例

https://github.com/guangqiang-liu/react-native-reduxDemo

上一節(jié)我們講解了Redux的常用API和如何發(fā)送同步Action,這節(jié)教程我們來講講如何發(fā)送異步的Action

Action發(fā)出以后,Reducer中立即計算出新的State,這種叫做同步;Action發(fā)出以后,過一段時間再執(zhí)行 Reducer,這就是異步。

怎么才能讓Reducer 在異步操作結(jié)束后自動執(zhí)行呢?這就要用到新的工具:中間件(middleware)。

下面這張圖展示了middleware的工作原理,可以好好的琢磨琢磨,理解這張圖的含義

middleware

中間件概念

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

  • Reducer:純函數(shù),只承擔計算 State 的功能,不合適承擔其他功能,也承擔不了,因為理論上,純函數(shù)不能進行讀寫操作。
  • View:與 State 一一對應(yīng),可以看作 State 的視覺層,也不合適承擔其他功能。
  • Action:存放數(shù)據(jù)的對象,即消息的載體,只能被別人操作,自己不能進行任何操作。

想來想去,只有在發(fā)送 Action 的這個步驟,即store.dispatch()方法,可以添加功能。舉例來說,要添加日志功能,把 Action 和 State 打印出來,可以對store.dispatch進行如下改造。

let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action);
  next(action);
  console.log('next state', store.getState());
}

上面代碼中,對store.dispatch進行了重定義,在發(fā)送 Action 前后添加了打印功能。這就是中間件的雛形。

中間件就是一個函數(shù),對store.dispatch方法進行了改造,在發(fā)出 Action 和執(zhí)行 Reducer 這兩步之間,添加了其他功能。

中間件用法

本教程不涉及如何編寫中間件,因為常用的中間件都有現(xiàn)成的,只要引用別人寫好的模塊即可。比如,上一節(jié)的日志中間件,就有現(xiàn)成的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()的功能增強。

這里有兩點需要注意:

  • createStore方法可以接受整個應(yīng)用的初始狀態(tài)作為參數(shù),那樣的話,applyMiddleware就是第三個參數(shù)了。
const store = createStore(
  reducer,
  initial_state,
  applyMiddleware(logger)
);
  • 中間件的次序有講究。
const store = createStore(
  reducer,
  applyMiddleware(thunk, promise, logger)
);

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

applyMiddlewares()

看到這里,你可能會問,applyMiddlewares這個方法到底是干什么的?

它是 Redux 的原生方法,作用是將所有中間件組成一個數(shù)組,依次執(zhí)行。下面是它的源碼。

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    var store = createStore(reducer, preloadedState, enhancer);
    var dispatch = store.dispatch;
    var chain = [];

    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    };
    chain = middlewares.map(middleware => middleware(middlewareAPI));
    dispatch = compose(...chain)(store.dispatch);

    return {...store, dispatch}
  }
}

上面代碼中,所有中間件被放進了一個數(shù)組chain,然后嵌套執(zhí)行,最后執(zhí)行store.dispatch。可以看到,中間件內(nèi)部(middlewareAPI)可以拿到getState和dispatch這兩個方法。

異步操作的基本思路

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

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

  • 操作發(fā)起時的 Action
  • 操作成功時的 Action
  • 操作失敗時的 Action

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

// 寫法一:名稱相同,參數(shù)不同
{ 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 也要進行改造,反映不同的操作狀態(tài)。下面是 State 的一個例子。

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

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

現(xiàn)在,整個異步操作的思路就很清楚了。

  • 操作開始時,送出一個 Action,觸發(fā) State 更新為"正在操作"狀態(tài),View 重新渲染
  • 操作結(jié)束后,再送出一個 Action,觸發(fā) State 更新為"操作結(jié)束"狀態(tài),View 再一次重新渲染

redux-thunk 中間件

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

奧妙就在 Action Creator 之中。

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

異步Action的示例

異步Action

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

下面就是fetchPosts的代碼,關(guān)鍵之處就在里面。

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(動作生成器),返回一個函數(shù)。這個函數(shù)執(zhí)行后,先發(fā)出一個Action(requestPosts(postTitle)),然后進行異步操作。拿到結(jié)果后,先將結(jié)果轉(zhuǎn)成 JSON 格式,然后再發(fā)出一個 Action( receivePosts(postTitle, json))。

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

  • fetchPosts返回了一個函數(shù),而普通的 Action Creator 默認返回一個對象。
  • 返回的函數(shù)的參數(shù)是dispatch和getState這兩個 Redux 方法,普通的 Action Creator 的參數(shù)是 Action 的內(nèi)容。
  • 在返回的函數(shù)之中,先發(fā)出一個 Action(requestPosts(postTitle)),表示操作開始。
  • 異步操作結(jié)束之后,再發(fā)出一個 Action(receivePosts(postTitle, json)),表示操作結(jié)束。

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

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

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';

// Note: this API requires redux@>=3.1.0
const store = createStore(
  reducer,
  applyMiddleware(thunk)
);

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

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

redux-promise 中間件

既然 Action Creator 可以返回函數(shù),當然也可以返回其他值。另一種異步操作的解決方案,就是讓 Action Creator 返回一個 Promise 對象。

這就需要使用redux-promise中間件。

import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import reducer from './reducers';

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

這個中間件使得store.dispatch方法可以接受 Promise 對象作為參數(shù)。這時,Action Creator 有兩種寫法。

  • 寫法一,返回值是一個 Promise 對象。
const fetchPosts = 
  (dispatch, postTitle) => new Promise(function (resolve, reject) {
     dispatch(requestPosts(postTitle));
     return fetch(`/some/API/${postTitle}.json`)
       .then(response => {
         type: 'FETCH_POSTS',
         payload: response.json()
       });
});
  • 寫法二,Action 對象的payload屬性是一個 Promise 對象。這需要從redux-actions模塊引入createAction方法,并且寫法也要變成下面這樣。
import { createAction } from 'redux-actions';

class AsyncApp extends Component {
  componentDidMount() {
    const { dispatch, selectedPost } = this.props
    // 發(fā)出同步 Action
    dispatch(requestPosts(selectedPost));
    // 發(fā)出異步 Action
    dispatch(createAction(
      'FETCH_POSTS', 
      fetch(`/some/API/${postTitle}.json`)
        .then(response => response.json())
    ));
  }

上面代碼中,第二個dispatch方法發(fā)出的是異步 Action,只有等到操作結(jié)束,這個 Action 才會實際發(fā)出。注意,createAction的第二個參數(shù)必須是一個 Promise 對象。

看一下redux-promise的源碼,就會明白它內(nèi)部是怎么操作的。

import { isFSA } from 'flux-standard-action';

function isPromise(val) {
  return val && typeof val.then === 'function';
}

export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action)
        ? action.then(dispatch)
        : next(action);
    }

    return isPromise(action.payload)
      ? action.payload.then(
          result => dispatch({ ...action, payload: result }),
          error => {
            dispatch({ ...action, payload: error, error: true });
            return Promise.reject(error);
          }
        )
      : next(action);
  };
}

從上面代碼可以看出,如果 Action 本身是一個 Promise,它 resolve 以后的值應(yīng)該是一個 Action 對象,會被dispatch方法送出(action.then(dispatch)),但 reject 以后不會有任何動作;如果 Action 對象的payload屬性是一個 Promise 對象,那么無論 resolve 和 reject,dispatch方法都會發(fā)出 Action。

總結(jié)

本系列教程是參照阮老師的Redux入門教程文章和Redux中文文檔進行的整合和拓展。更多更詳細的Redux使用方式請參照教程一中的參考文獻。

福利時間

  • 作者React Native開源項目OneM地址(按照企業(yè)開發(fā)標準搭建框架設(shè)計開發(fā)):https://github.com/guangqiang-liu/OneM (歡迎小伙伴們 star)
  • 作者簡書主頁:包含50多篇RN開發(fā)相關(guān)的技術(shù)文章http://www.lxweimin.com/u/023338566ca5 (歡迎小伙伴們:多多關(guān)注多多點贊)
  • 作者React Native QQ技術(shù)交流群:620792950 歡迎小伙伴進群交流學習
  • 友情提示:在開發(fā)中有遇到RN相關(guān)的技術(shù)問題,歡迎小伙伴加入交流群(620792950),在群里提問、互相交流學習。交流群也定期更新最新的RN學習資料給大家,謝謝支持!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,334評論 2 377

推薦閱讀更多精彩內(nèi)容

  • 一、什么情況需要redux? 1、用戶的使用方式復雜 2、不同身份的用戶有不同的使用方式(比如普通用戶和管...
    初晨的筆記閱讀 2,048評論 0 11
  • 看到這篇文章build an image gallery using redux saga,覺得寫的不錯,長短也適...
    smartphp閱讀 6,194評論 1 29
  • 學習必備要點: 首先弄明白,Redux在使用React開發(fā)應(yīng)用時,起到什么作用——狀態(tài)集中管理 弄清楚Redux是...
    賀賀v5閱讀 8,930評論 10 58
  • 原文地址在我的博客, 轉(zhuǎn)載請注明出處,謝謝! 概述 本文介紹了我對 Redux 狀態(tài)管理的思想、原理、架構(gòu)方法的認...
    莫凡_Tcg閱讀 6,853評論 0 15
  • 一月詩人的右手上煮了一把傘 二月的發(fā)上開出了薔薇 三月是麻雀熙熙攘攘越過樓房 四月很快向你告別 五月說他想要去流浪...
    半生南望閱讀 295評論 0 0