## 異步 Action
在 基礎教程 中,我們創建了一個簡單的 todo 應用。它只有同步操作。每當 dispatch action 時,state 會被立即更新。
在本教程中,我們將開發一個不同的,異步的應用。它將使用 Reddit API 來獲取并顯示指定 subreddit 下的帖子列表。那么 Redux 究竟是如何處理異步數據流的呢?
### Action
當調用異步 API 時,有兩個非常關鍵的時刻:發起請求的時刻,和接收到響應的時刻(也可能是超時)。
這兩個時刻都可能會更改應用的 state;為此,你需要 dispatch 普通的同步 action。一般情況下,每個 API 請求都需要 dispatch 至少三種 action:
* 一種通知 reducer 請求開始的 action。
對于這種 action,reducer 可能會切換一下 state 中的 isFetching 標記。以此來告訴 UI 來顯示加載界面。
* 一種通知 reducer 請求成功的 action。
對于這種 action,reducer 可能會把接收到的新數據合并到 state 中,并重置 isFetching。UI 則會隱藏加載界面,并顯示接收到的數據。
* 一種通知 reducer 請求失敗的 action。
對于這種 action,reducer 可能會重置 isFetching。另外,有些 reducer 會保存這些失敗信息,并在 UI 里顯示出來。
為了區分這三種 action,可能在 action 里添加一個專門的 status 字段作為標記位:
~~~
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }
~~~
又或者為它們定義不同的 type:
~~~
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
~~~
究竟使用帶有標記位的同一個 action,還是多個 action type 呢,完全取決于你。這應該是你的團隊共同達成的約定。使用多個 type 會降低犯錯誤的機率,但是如果你使用像 redux-actions 這類的輔助庫來生成 action 創建函數和 reducer 的話,這就完全不是問題了。
無論使用哪種約定,一定要在整個應用中保持統一。
在本教程中,我們將使用不同的 type 來做。
### 同步 Action 創建函數(Action Creator)
下面先定義幾個同步的 action 類型 和 action 創建函數。比如,用戶可以選擇要顯示的 subreddit:
~~~
// actions.js
export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'
export function selectSubreddit(subreddit) {
return {
type: SELECT_SUBREDDIT,
subreddit
}
}
~~~
也可以按 "刷新" 按鈕來更新它:
~~~
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function invalidatesubreddit(subreddit) {
return {
type: INVALIDATE_SUBREDDIT,
subreddit
}
}
~~~
這些是用戶操作來控制的 action。也有另外一類 action,是由網絡請求來控制。后面會介紹如何使用它們,現在,我們只是來定義它們。
當需要獲取指定 subreddit 的帖子的時候,需要 dispatch REQUEST_POSTS action:
~~~
export const REQUEST_POSTS = 'REQUEST_POSTS'
export function requestPosts(subreddit) {
return {
type: REQUEST_POSTS,
subreddit
}
}
~~~
把 `REQUEST_POSTS` 和 `SELECT_SUBREDDIT` 或 `INVALIDATE_SUBREDDIT` 分開很重要。雖然它們的發生有先后順序,但隨著應用變得復雜,有些用戶操作(比如,預加載最流行的 subreddit,或者一段時間后自動刷新過期數據)后需要馬上請求數據。路由變化時也可能需要請求數據,所以一開始如果把請求數據和特定的 UI 事件耦合到一起是不明智的。
最后,當收到請求響應時,我們會 dispatch `RECEIVE_POSTS`:
~~~
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
export function receivePosts(subreddit, json) {
return {
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}
~~~
以上就是現在需要知道的所有內容。稍后會介紹如何把 dispatch action 與網絡請求結合起來。
### 錯誤處理須知
在實際應用中,網絡請求失敗時也需要 dispatch action。雖然在本教程中我們并不做錯誤處理,但是這個 真實場景的案例 會演示一種實現方案。
### 設計 state 結構
就像在基礎教程中,在功能開發前你需要 設計應用的 state 結構。在寫異步代碼的時候,需要考慮更多的 state,所以我們要仔細考慮一下。
這部分內容通常讓初學者感到迷惑,因為選擇哪些信息才能清晰地描述異步應用的 state 并不直觀,還有怎么用一個樹來把這些信息組織起來。
我們以最通用的案例來打頭:列表。Web 應用經常需要展示一些內容的列表。比如,帖子的列表,朋友的列表。首先要明確應用要顯示哪些列表。然后把它們分開儲存在 state 中,這樣你才能對它們分別做緩存并且在需要的時候再次請求更新數據。
"Reddit 頭條" 應用會長這個樣子:
~~~
{
selectedsubreddit: 'frontend',
postsBySubreddit: {
frontend: {
isFetching: true,
didInvalidate: false,
items: []
},
reactjs: {
isFetching: false,
didInvalidate: false,
lastUpdated: 1439478405547,
items: [
{
id: 42,
title: 'Confusion about Flux and Relay'
},
{
id: 500,
title: 'Creating a Simple Application Using React JS and Flux Architecture'
}
]
}
}
}
~~~
下面列出幾個要點:
* 分開存儲 subreddit 信息,是為了緩存所有 subreddit。當用戶來回切換 subreddit 時,可以立即更新,同時在不需要的時候可以不請求數據。不要擔心把所有帖子放到內存中(會浪費內存):除非你需要處理成千上萬條帖子,同時用戶還很少關閉標簽頁,否則你不需要做任何清理。
* 每個帖子的列表都需要使用 isFetching 來顯示進度條,didInvalidate 來標記數據是否過期,lastUpdated 來存放數據最后更新時間,還有 items 存放列表信息本身。在實際應用中,你還需要存放 fetchedPageCount 和 nextPageUrl 這樣分頁相關的 state。
> 嵌套內容須知
在這個示例中,接收到的列表和分頁信息是存在一起的。但是,這種做法并不適用于有互相引用的嵌套內容的場景,或者用戶可以編輯列表的場景。想像一下用戶需要編輯一個接收到的帖子,但這個帖子在 state tree 的多個位置重復出現。這會讓開發變得非常困難。
> 如果你有嵌套內容,或者用戶可以編輯接收到的內容,你需要把它們分開存放在 state 中,就像數據庫中一樣。在分頁信息中,只使用它們的 ID 來引用。這可以讓你始終保持數據更新。真實場景的案例 中演示了這種做法,結合 normalizr 來把嵌套的 API 響應數據范式化,最終的 state 看起來是這樣:
~~~
{
selectedsubreddit: 'frontend',
entities: {
users: {
2: {
id: 2,
name: 'Andrew'
}
},
posts: {
42: {
id: 42,
title: 'Confusion about Flux and Relay',
author: 2
},
100: {
id: 100,
title: 'Creating a Simple Application Using React JS and Flux Architecture',
author: 2
}
}
},
postsBySubreddit: {
frontend: {
isFetching: true,
didInvalidate: false,
items: []
},
reactjs: {
isFetching: false,
didInvalidate: false,
lastUpdated: 1439478405547,
items: [ 42, 100 ]
}
}
}
~~~
在本教程中,我們不會對內容進行范式化,但是在一個復雜些的應用中你可能需要使用。
### 處理 Action
在講 dispatch action 與網絡請求結合使用細節前,我們為上面定義的 action 開發一些 reducer。
Reducer 組合須知
這里,我們假設你已經學習過 combineReducers() 并理解 reducer 組合,還有 基礎章節 中的 拆分 Reducer。如果還沒有,請 先學習。
~~~
//reducers.js
import { combineReducers } from 'redux'
import {
SELECT_SUBREDDIT,
INVALIDATE_SUBREDDIT,
REQUEST_POSTS,
RECEIVE_POSTS
} from '../actions'
function selectedsubreddit(state = 'reactjs', action) {
switch (action.type) {
case SELECT_SUBREDDIT:
return action.subreddit
default:
return state
}
}
function posts(
state = {
isFetching: false,
didInvalidate: false,
items: []
},
action
) {
switch (action.type) {
case INVALIDATE_SUBREDDIT:
return Object.assign({}, state, {
didInvalidate: true
})
case REQUEST_POSTS:
return Object.assign({}, state, {
isFetching: true,
didInvalidate: false
})
case RECEIVE_POSTS:
return Object.assign({}, state, {
isFetching: false,
didInvalidate: false,
items: action.posts,
lastUpdated: action.receivedAt
})
default:
return state
}
}
function postsBySubreddit(state = {}, action) {
switch (action.type) {
case INVALIDATE_SUBREDDIT:
case RECEIVE_POSTS:
case REQUEST_POSTS:
return Object.assign({}, state, {
[action.subreddit]: posts(state[action.subreddit], action)
})
default:
return state
}
}
const rootReducer = combineReducers({
postsBySubreddit,
selectedsubreddit
})
export default rootReducer
~~~
上面代碼有兩個有趣的點:
使用 ES6 計算屬性語法,使用 Object.assign() 來簡潔高效地更新 state[action.subreddit]。這個:
~~~
return Object.assign({}, state, {
[action.subreddit]: posts(state[action.subreddit], action)
})
~~~
與下面代碼等價:
~~~
let nextState = {}
nextState[action.subreddit] = posts(state[action.subreddit], action)
return Object.assign({}, state, nextState)
~~~
我們提取出 posts(state, action) 來管理指定帖子列表的 state。這就是 reducer 組合 !我們還可以借此機會把 reducer 分拆成更小的 reducer,這種情況下,我們把對象內列表的更新代理到了 posts reducer 上。在 真實場景的案例 中甚至更進一步,里面介紹了如何做一個 reducer 工廠來生成參數化的分頁 reducer。
記住 reducer 只是函數而已,所以你可以盡情使用函數組合和高階函數這些特性。
### 異步 action 創建函數
最后,如何把 之前定義 的同步 action 創建函數和網絡請求結合起來呢?標準的做法是使用 Redux Thunk 中間件。要引入 redux-thunk 這個專門的庫才能使用。我們 后面 會介紹 middleware 大體上是如何工作的;目前,你只需要知道一個要點:通過使用指定的 middleware,action 創建函數除了返回 action 對象外還可以返回函數。這時,這個 action 創建函數就成為了 thunk。
當 action 創建函數返回函數時,這個函數會被 Redux Thunk middleware 執行。這個函數并不需要保持純凈;它還可以帶有副作用,包括執行異步 API 請求。這個函數還可以 dispatch action,就像 dispatch 前面定義的同步 action 一樣。
我們仍可以在 actions.js 里定義這些特殊的 thunk action 創建函數。
~~~
// actions.js
import fetch from 'cross-fetch'
export const REQUEST_POSTS = 'REQUEST_POSTS'
function requestPosts(subreddit) {
return {
type: REQUEST_POSTS,
subreddit
}
}
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(subreddit, json) {
return {
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}
export const INVALIDATE_SUBREDDIT = ‘INVALIDATE_SUBREDDIT’
export function invalidateSubreddit(subreddit) {
return {
type: INVALIDATE_SUBREDDIT,
subreddit
}
}
// 來看一下我們寫的第一個 thunk action 創建函數!
// 雖然內部操作不同,你可以像其它 action 創建函數 一樣使用它:
// store.dispatch(fetchPosts('reactjs'))
export function fetchPosts(subreddit) {
// Thunk middleware 知道如何處理函數。
// 這里把 dispatch 方法通過參數的形式傳給函數,
// 以此來讓它自己也能 dispatch action。
return function (dispatch) {
// 首次 dispatch:更新應用的 state 來通知
// API 請求發起了。
dispatch(requestPosts(subreddit))
// thunk middleware 調用的函數可以有返回值,
// 它會被當作 dispatch 方法的返回值傳遞。
// 這個案例中,我們返回一個等待處理的 promise。
// 這并不是 redux middleware 所必須的,但這對于我們而言很方便。
return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
.then(
response => response.json(),
// 不要使用 catch,因為會捕獲
// 在 dispatch 和渲染中出現的任何錯誤,
// 導致 'Unexpected batch number' 錯誤。
// https://github.com/facebook/react/issues/6895
error => console.log('An error occurred.', error)
)
.then(json =>
// 可以多次 dispatch!
// 這里,使用 API 請求結果來更新應用的 state。
dispatch(receivePosts(subreddit, json))
)
}
}
~~~
### fetch 使用須知
本示例使用了 fetch API。它是替代 XMLHttpRequest 用來發送網絡請求的非常新的 API。由于目前大多數瀏覽器原生還不支持它,建議你使用 cross_fetch 庫:
// 每次使用 `fetch` 前都這樣調用一下
import fetch from 'cross_fetch'
在底層,它在瀏覽器端使用 whatwg-fetch polyfill,在服務器端使用 node-fetch,所以如果當你把應用改成 同構 時,并不需要改變 API 請求。
注意,fetch polyfill 假設你已經使用了 Promise 的 polyfill。確保你使用 Promise polyfill 的一個最簡單的辦法是在所有應用代碼前啟用 Babel 的 ES6 polyfill:
// 在應用中其它任何代碼執行前調用一次
import 'babel-polyfill'
我們是如何在 dispatch 機制中引入 Redux Thunk middleware 的呢?我們使用了 applyMiddleware(),如下:
~~~
// index.js
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'
import { selectSubreddit, fetchPosts } from './actions'
import rootReducer from './reducers'
const loggerMiddleware = createLogger()
const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware, // 允許我們 dispatch() 函數
loggerMiddleware // 一個很便捷的 middleware,用來打印 action 日志
)
)
store.dispatch(selectSubreddit('reactjs'))
store
.dispatch(fetchPosts('reactjs'))
.then(() => console.log(store.getState())
)
~~~
thunk 的一個優點是它的結果可以再次被 dispatch:
~~~
// actions.js
import fetch from 'cross-fetch'
export const REQUEST_POSTS = 'REQUEST_POSTS'
function requestPosts(subreddit) {
return {
type: REQUEST_POSTS,
subreddit
}
}
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(subreddit, json) {
return {
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function invalidateSubreddit(subreddit) {
return {
type: INVALIDATE_SUBREDDIT,
subreddit
}
}
function fetchPosts(subreddit) {
return dispatch => {
dispatch(requestPosts(subreddit))
return fetch(`http://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(subreddit, json)))
}
}
function shouldFetchPosts(state, subreddit) {
const posts = state.postsBySubreddit[subreddit]
if (!posts) {
return true
} else if (posts.isFetching) {
return false
} else {
return posts.didInvalidate
}
}
export function fetchPostsIfNeeded(subreddit) {
// 注意這個函數也接收了 getState() 方法
// 它讓你選擇接下來 dispatch 什么。
// 當緩存的值是可用時,
// 減少網絡請求很有用。
return (dispatch, getState) => {
if (shouldFetchPosts(getState(), subreddit)) {
// 在 thunk 里 dispatch 另一個 thunk!
return dispatch(fetchPosts(subreddit))
} else {
// 告訴調用代碼不需要再等待。
return Promise.resolve()
}
}
}
~~~
這可以讓我們逐步開發復雜的異步控制流,同時保持代碼整潔如初:
~~~
index.js
store
.dispatch(fetchPostsIfNeeded('reactjs'))
.then(() => console.log(store.getState())
)
~~~
### 服務端渲染須知
異步 action 創建函數對于做服務端渲染非常方便。你可以創建一個 store,dispatch 一個異步 action 創建函數,這個 action 創建函數又 dispatch 另一個異步 action 創建函數來為應用的一整塊請求數據,同時在 Promise 完成和結束時才 render 界面。然后在 render 前,store 里就已經存在了需要用的 state。
Thunk middleware 并不是 Redux 處理異步 action 的唯一方式:
* 你可以使用 redux-promise 或者 redux-promise-middleware 來 dispatch Promise 來替代函數。
* 你可以使用 redux-observable 來 dispatch Observable。
* 你可以使用 redux-saga 中間件來創建更加復雜的異步 action。
* 你可以使用 redux-pack 中間件 dispatch 基于 Promise 的異步 Action。
* 你甚至可以寫一個自定義的 middleware 來描述 API 請求,就像這個 真實場景的案例 中的做法一樣。
* 你也可以先嘗試一些不同做法,選擇喜歡的,并使用下去,不論有沒有使用到 middleware 都行。
### 連接到 UI
Dispatch 同步 action 與異步 action 間并沒有區別,所以就不展開討論細節了。參照 搭配 React 獲得 React 組件中使用 Redux 的介紹。參照 示例:Reddit API 來獲取本例的完整代碼。
下一步
閱讀 異步數據流 來整理一下異步 action 是如何適用于 Redux 數據流的。
- js
- js繼承
- keyCode
- 好的網站
- 零散知識點-js
- This
- 對象深拷貝和淺拷貝
- 數組方法
- 數組的深拷貝和淺拷貝
- JS 引擎的執行機制
- js中的new
- 常用正則
- 函數柯里化
- 會修改當前數組的方法
- 不會修改當前數組的方法
- 函數式編程
- 循環遍歷
- 基礎知識
- 異步
- js知識總結
- fileReader
- HTML
- 零散知識點
- html5新特性
- viewport
- CSS
- cursor
- css3新特性
- 水平居中
- 垂直居中
- display解析
- 塊級元素和行內元素
- css技巧和方法
- 清除浮動
- Less
- Sass
- 綜合
- 微信小程序
- 前端面試
- CSS-面試
- JS-面試
- js-web-api
- js知識
- MVC-面試
- jQuery與框架的區別
- 閉包
- promise
- http狀態碼
- cdn
- 離線存儲
- 事件
- web安全
- 性能優化
- 響應式
- 服務器渲染和本地渲染
- 模板是什么?
- VUE流程
- 瀏覽器渲染過程
- this的指向
- new的使用
- HTML-面試
- title和alt區別
- html5元素
- h5新特性
- 圖片格式
- 零散面試總結
- react
- 生命周期-react
- state
- props
- 組件通信
- 虛擬DOM
- 源碼分析
- webstorm-template
- element與component區別
- 組件的理解
- JXS
- vue與react區別
- 16.8版本
- vue
- 生命周期-vue
- 實現流程
- webpack
- 概念
- 入口起點
- 出口
- loader
- 模式
- 插件
- manifest
- redux
- 介紹
- 核心概念
- 三大原則
- 基礎
- action
- reducer
- store
- 數據流
- 高級
- 異步action
- 異步數據流
- middleware
- ES6阮一峰
- ...
- let
- es6箭頭函數
- const
- 塊級作用域
- 頂層對象的屬性
- global 對象
- 變量的解構賦值
- 字符串的擴展
- promise對象
- 正則的擴展
- 數值的擴展
- Math對象的擴展
- 函數的擴展
- 數組的擴展
- 對象的擴展
- symbol
- async函數
- class的基本用法
- Class 的繼承
- Set 和 Map 數據結構
- 開發工具
- 好用的軟件
- chrome插件
- 其他實用工具
- 微信公眾號-前端早讀課
- 【第1352期】map和reduce,處理數據結構的利器
- 微信公眾號-前端大全
- JS 的執行機制
- 一篇文章理解 JS 繼承
- 瀏覽器
- 緩存
- 《Webkit技術內幕》之頁面渲染過程
- 跨域
- 安全
- XSS
- 設計模式
- 發布訂閱模式
- 工廠模式
- MV*模式
- 觀察者模式
- react-router
- 一些小技巧
- js一些小算法
- 1.已知一個數組中的值,在另外一個數組中查找該值
- 累加器
- 數組隨機
- 數組扁平化并去重排序
- Immutable
- 常用命令
- hybrid
- schema封裝
- typescript