// 章節 0 - introduction.js
// 你可能已經看過這張著名的 flux 的單向數據流圖了。
// 在這個教程里,我們會一步步地向你介紹上圖里的各個概念。
// 我們會把這些概念分成單獨的章節來介紹它們存在的意義和作用。
// 在最后,當我們理解了每一個概念后,我們會發現這張圖真是意義深遠啊!
// 在我們開始之前,我們先聊下一 flux 存在的意義以及我們為什么需要它。
// 假設我們正在構建一個網站應用,那么這個網站應用會由什么組成呢?
// 1) 模板/HTML = View
// 2) 填充視圖的數據 = Model
// 3) 獲取數據、將所有視圖組裝在一起、響應用戶事件、
// 數據操作等等的邏輯 = Controller
// 這是我們熟知的非常典型的 MVC,但它和 flux 的概念其實是很像的,
// 只是在某些表述上有些小小的不同:
// - Model 看起來像 Store
// - 用戶事件、數據操作以及它們的處理程序看起來像
// "action creators" -> action -> dispatcher -> callback
// - View 看起來像 React view (或者其它類似的概念)
// 所以,flux 就只是一個新名詞么?不全是,但是新名詞是很重要的,
// 因為通過引入這些新術語我們可以更准確地表述各種專業術語。
// 舉一個例子,獲取數據是一個 action,一個點擊是一個 action,
// 一個 input 變化也是一個 action 等等。我們都已經習慣了從我們的應用里分發 action,
// 只是以不同的方式稱呼它們。 不同於直接修改 Model 和 View,
// Flux 確保所有 action 首先通過一個 dispatcher,
// 然后再是 store,最后通知所有的 store 監聽器。
// 為了弄清楚 MVC 和 flux 的不同,
// 我們舉一個典型的 MVC 應用的用例:
// 一個典型的 MVC 應用的流程大致上是這樣的:
// 1) 用戶點擊按鈕 A
// 2) 點擊按鈕 A 的處理程序觸發 Model A 的改變
// 3) Model A 的改變處理程序觸發 Model B 的改變
// 4) Model B 的改變處理程序觸發 View B 的改變並重新渲染自身
// 在這樣的一個環境里,當應用出錯的時候快速地定位 bug 來源是一件非常困難的事情。
// 這是因為每個 View 可以監視任何的 Model,
// 並且每個 Model 可以監視其它所有 Model,所以數據會從四面八方涌來,並且被許多源(view 或者 model)改變。
// 當我們用 flux 以及它的單向數據流的時候,上面的例子就會變成這樣子:
// 1) 用戶點擊按鈕 A
// 2) 點擊按鈕A的處理程序會觸發一個被分發的 action,並改變 Store A
// 3) 因為其它的 Store 也被這個 action 通知了,所以 Store B 也會對相同的 action 做出反應
// 4) View B 因為 Store A 和 Store B 的改變而收到通知,並重新渲染
// 來看一下我們是如何避免 Store A 和 Store B 直接相關聯的。
// Store 只能被 action 修改,別無他選。
// 並且當所有 Store 響應了 action 后,View 才會最終更新。由此可見,數據總是沿着一個方向進行流動:
// action -> store -> view -> action -> store -> view -> action -> ...
// 上面我們首先從 action 開始我們的用例,
// 下面讓我們同樣以 action 和 action creator 來開始我們的教程。
// 章節 1 - simple-action-creator.js
// 我們在前言中已經簡單提到過 action,但具體什么是 action creator,它們又是如何關聯到 action 的呢?
// 其實,通過幾行簡單的代碼就可以解釋清楚了!
// action creator 就是函數而已...
var actionCreator = function() {
// ...負責構建一個 action (是的,action creator 這個名字已經很明顯了)並返回它
return {
type: 'AN_ACTION'
}
}
// 這就完了?是的,僅此而已。
// 然而,有一件事情需要注意,那就是 action 的格式。flux 一般約定 action 是一個擁有 type 屬性的對象。
// 然后按 type 決定如何處理 action。當然,action 依舊可以擁有其他屬性,你可以任意存放想要的數據。
// 在后面的章節中,我們會發現 action creator 實際上可以返回 action 以外的其他東西,比如一個函數。
// 這在處理異步時很有用(更多的內容可以查閱 dispatch-async-action.js)。
// 我們可以直接調用 action creator,如同預期的一樣,我們會得到一個 action:
console.log(actionCreator())
// 輸出: { type: 'AN_ACTION' }
// 好了,以上代碼沒有任何問題,卻也毫無用處...
// 在實際的場景中,我們需要的是將 action 發送到某個地方,讓關心它的人知道發生了什么,並且做出相應的處理。
// 我們將這個過程稱之為“分發 action(Dispatching an action)”。
// 為了分發 action,我們需要...一個分發函數(= ̄ω ̄=)。
// 並且,為了讓任何對它感興趣的人都能感知到 action 發起,我們還需要一個注冊“處理器(handlers)”的機制。
// 這些 action 的“處理器”在傳統的 flux 應用中被稱為 store,在下個章節中,我們會介紹它們在 Redux 中叫什么。
// 至止,我們的應用中包含了以下流程:
// ActionCreator -> Action
// 章節 2 - about-state-and-meet-redux.js
// 在實際應用中,我們不僅需要 action 告訴我們發生了什么,還要告訴我們需要隨之更新數據。
// 這就讓我們的應用變的棘手:
// 如何在應用程序的整個生命周期內維持所有數據?
// 如何修改這些數據?
// 如何把數據變更傳播到整個應用程序?
// 於是 Redux 登場。
// Redux (https://github.com/reactjs/redux) 是一個“可預測化狀態的 JavaScript 容器”。
// 我們先回顧上述提出的問題並用 Redux 的詞匯表給出以下解答(部分詞匯也來源於 Flux):
// 如何在應用程序的整個生命周期內維持所有數據?
// 以你想要的方式維持這些數據,例如 JavaScript 對象、數組、不可變數據,等等。
// 我們把應用程序的數據稱為狀態。這是有道理的,因為我們所說的數據會隨着時間的推移發生變化,這其實就是應用的狀態。
// 但是我們把這些狀態信息轉交給了 Redux(還記得么?Redux 就是一個“容納狀態的容器”)。
// 如何修改這些數據?
// 我們使用 reducer 函數修改數據(在傳統的 Flux 中我們稱之為 store)。
// Reducer 函數是 action 的訂閱者。
// Reducer 函數只是一個純函數,它接收應用程序的當前狀態以及發生的 action,然后返回修改后的新狀態(或者有人稱之為歸並后的狀態)。
// 如何把數據變更傳播到整個應用程序?
// 使用訂閱者來監聽狀態的變更情況。
// Redux 幫你把這些連接起來。
// 總之 Redux 提供了:
// 1)存放應用程序狀態的容器
// 2)一種把 action 分發到狀態修改器的機制,也就是 reducer 函數
// 3)監聽狀態變化的機制
// 我們把 Redux 實例稱為 store 並用以下方式創建:
/*
import { createStore } from 'redux'
var store = createStore()
*/
// 但是當你運行上述代碼,你會發現以下異常消息:
// Error: Invariant Violation: Expected the reducer to be a function.
// 這是因為 createStore 函數必須接收一個能夠修改應用狀態的函數。
// 我們再試一下
import { createStore } from 'redux'
var store = createStore(() => {})
// 看上去沒有問題了...
// 章節 3 - simple-reducer.js
// 現在,我們知道如何去創建一個 Redux 實例,並讓它管理應用中的 state
// 下面講一下這些 reducer 函數是如何轉換 state 的。
// Reducer 與 Store 區別:
// 你可能已經注意到,在簡介章節中的 Flux 圖表中,有 Store,但沒有
// Redux 中的 Reducer。那么,Store 與 Reducer 到底有哪些區別呢?
// 實際的區別比你想象的簡單:Store 可以保存你的 data,而 Reducer 不能。
// 因此在傳統的 Flux 中,Store 本身可以保存 state,但在 Redux 中,每次調用 reducer
// 時,都會傳入待更新的 state。這樣的話,Redux 的 store 就變成了
// “無狀態的 store” 並且改了個名字叫 Reducer。
// 如上所述,在創建一個 Redux 實例前,需要給它一個 reducer 函數...
import { createStore } from 'redux'
var store_0 = createStore(() => {})
// ...所以每當一個 action 發生時,Redux 都能調用這個函數。
// 往 createStore 傳 Reducer 的過程就是給 Redux 綁定 action 處理函數(也就是 Reducer)的過程。
// action 處理函數在 01_simple-action-creator.js 章節中有討論過。
// 在 Reducer 中打印一些 log
var reducer = function (...args) {
console.log('Reducer was called with args', args)
}
var store_1 = createStore(reducer)
// 輸出:Reducer was called with args [ undefined, { type: '@@redux/INIT' } ]
// 看出來了嗎?我們的 reducer 被調用了,但我們並沒有 dispatch 任何 action...
// 這是因為在初始化應用 state 的時候,
// Redux dispatch 了一個初始化的 action ({ type: '@@redux/INIT' })
// 在被調用時,一個 reducer 會得到這些參數:(state, action)
// 在應用初始化時,state 還沒被初始化,因此它的值是 "undefined",
// 這是非常符合邏輯的
// 在處理 “init” action 之后,我們應用中的 state 又會是怎么樣的呢?
// 章節 4 - get-state.js
// 如何從 Redux 實例中讀取 state ?
import { createStore } from 'redux'
var reducer_0 = function (state, action) {
console.log('reducer_0 was called with state', state, 'and action', action)
}
var store_0 = createStore(reducer_0)
// 輸出: reducer_0 was called with state undefined and action { type: '@@redux/INIT' }
// 為了讀取 Redux 保存的 state,你可以調用 getState
console.log('store_0 state after initialization:', store_0.getState())
// 輸出: store_0 state after initialization: undefined
// 都已經初始化過了,難道程序的 state 還是 undefined 的?沒錯,正是如此,
// 到目前為止,我們的 reducer 還什么事都沒做過…… 你是否還有印象,我們在 "about-state-and-meet-redux" 那一章里是怎么描述一個 reducer 的預期行為的?
// “一個 reducer 只是一個函數,它能收到程序當前的 state 與 action,
// 然后返回一個 modify(又或者學別人一樣稱之為 reduce )過的新 state ”
// 我們的 reducer 目前什么都不返回,所以程序的 state 當然只能是 reducer() 返回的那個叫 “undefined” 的東西。
// 接下來,我們試着在 reducer 收到 undefined 的 state 時,給程序發一個初始狀態:
var reducer_1 = function (state, action) {
console.log('reducer_1 was called with state', state, 'and action', action)
if (typeof state === 'undefined') {
return {}
}
return state;
}
var store_1 = createStore(reducer_1)
// 輸出:reducer_1 was called with state undefined and action { type: '@@redux/INIT' }
console.log('store_1 state after initialization:', store_1.getState())
// 輸出:store_1 state after initialization: {}
// 如我們所願,現在 Redux 初始化以后返回的 state 變成 {} 了
//
// 感謝ES6,這個模式現在實現起來很清晰:
var reducer_2 = function (state = {}, action) {
console.log('reducer_2 was called with state', state, 'and action', action)
return state;
}
var store_2 = createStore(reducer_2)
// 輸出: reducer_2 was called with state {} and action { type: '@@redux/INIT' }
console.log('store_2 state after initialization:', store_2.getState())
// 輸出: store_2 state after initialization: {}
// 估計你已經發現了,我們給 reducer_2 的 state 參數傳了默認值之后,
// reducer 就不會再取到 undefined 的 state 了。
// 小結一下:調用 reducer ,只是為了響應一個派發來的 action 。
// 接下來,我們在 response 里模擬一個 state 修改,其響應的 action 類型是 'SAY_SOMETIHG'
var reducer_3 = function (state = {}, action) {
console.log('reducer_3 was called with state', state, 'and action', action)
switch (action.type) {
case 'SAY_SOMETHING':
return {
...state,
message: action.value
}
default:
return state;
}
}
var store_3 = createStore(reducer_3)
// 輸出: reducer_3 was called with state {} and action { type: '@@redux/INIT' }
console.log('store_3 state after initialization:', store_3.getState())
// 輸出: store_3 state after initialization: {}
// 到目前為止,我們都還沒有得到一個新 state, 因為我們還沒有真的派發過任何 action 。
// 不過在最后一個例子里,有幾個點值得注意:
//
// 0) 我假設了 action 里一定包含了一個 type 跟一個 value 。type 基本上是 flux action 已經約定俗成的,
// 而 value 屬性可以是任何類型的。
// 1) 這里有個常見模式:在 reducer 里用 switch 來響應對應的 action 。
// 2) 用 switch 的時候, **永遠** 不要忘記放個 “default” 來返回 “state”,否則,
// 你的 reducer 可能會返回 “undefined” (等於你的 state 就丟了)
// 3) 注意 { message: action.value } 是怎么被合並到當前 state 來形成新 state 的,
// 這全要感謝牛逼的 ES7 notation (Object Spread): { ...state, message: action.value }
// 4) 還要注意:之所以這個例子能用ES7 Object Spread notation ,是因為它只對 state 里的
// { message: action.value} 做了淺拷貝(也就是說, state 第一個層級的屬性直接被 { message: action.value } 覆蓋掉了 —— 與之相對,其實也有優雅的合並方式 )
// 但是如果數據結構更復雜或者是嵌套的,那處理state更新的時候,很可能還需要考慮一些完全不同的做法:
// - 可以考慮: Immutable.js (https://facebook.github.io/immutable-js/)
// - 可以考慮: Object.assign (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
// - 可以考慮: 手工合並
// - 又或者考慮用其它任何能滿足需要且適合 state 結構的方法,Redux 對此是全無預設的方式的(要記得 Redux 只是個狀態的容器)。
// 現在開始,我們要在 reducer 里處理 action 了,我們將會有多個 reducer 並會組合它們。
// 章節 5 - combine-reducers.js
// 我們現在來看一下什么是 reducer
var reducer_0 = function (state = {}, action) {
console.log('reducer_0 was called with state', state, 'and action', action)
switch (action.type) {
case 'SAY_SOMETHING':
return {
...state,
message: action.value
}
default:
return state;
}
}
// 在繼續之前,我們先來想象一下擁有很多 action 的 reducer 長什么樣子
var reducer_1 = function (state = {}, action) {
console.log('reducer_1 was called with state', state, 'and action', action)
switch (action.type) {
case 'SAY_SOMETHING':
return {
...state,
message: action.value
}
case 'DO_SOMETHING':
// ...
case 'LEARN_SOMETHING':
// ...
case 'HEAR_SOMETHING':
// ...
case 'GO_SOMEWHERE':
// ...
// etc.
default:
return state;
}
}
// 很顯然,只有一個 reducer 是 hold 不住我們整個應用中所有 action 操作的(好吧,事實上它能 hold 得住,
// 但這會變得很難維護。)
// 幸運的是,Redux 不關心我們到底是只有一個 reducer ,還是有一打(12個)reducer 。
// 如果我們有多個 reducer ,Redux 能幫我們合並成一個。
// 讓我們來定義 2 個 reducer
var userReducer = function (state = {}, action) {
console.log('userReducer was called with state', state, 'and action', action)
switch (action.type) {
// etc.
default:
return state;
}
}
var itemsReducer = function (state = [], action) {
console.log('itemsReducer was called with state', state, 'and action', action)
switch (action.type) {
// etc.
default:
return state;
}
}
// 我希望你特別留意賦給每個 reducer 的初始 state :
// 1. 賦給 userReducer 的初始 state 是一個空對象,即 {}
// 2. 賦給 itemsReducer 的初始 state 是一個空數組,即 []
// 賦予不同類型的值是為了說明 reducer 是可以處理任何類型的數據結構的。你完全可以選擇那些符合你的需求的
// 數據結構作為 state 的值。(例如,字面量對象、數組、布爾值、字符串或其它不可變結構)
// 在這種多個 reducer 的模式下,我們可以讓每個 reducer 只處理整個應用的部分 state 。
// 但我們需要知道,createStore 只接收一個 reducer 函數。
// 那么,我們怎么合並所有的 reducer? 我們又該如何告訴 Redux 每個 reducer 只處理一部分 state 呢?
// 其實這很簡單。我們使用 combineReducers 輔助函數。
// combineReducers 接收一個對象並返回一個函數,當 combineReducers 被調用時,它會去調用每個
// reducer,並把返回的每一塊 state 重新組合成一個大 state 對象(也就是 Redux 中的 Store)。
// 長話短說,下面演示一下如何使用多個 reducer 創建一個 Redux 實例:
import { createStore, combineReducers } from 'redux'
var reducer = combineReducers({
user: userReducer,
items: itemsReducer
})
// 輸出:
// userReducer was called with state {} and action { type: '@@redux/INIT' }
// userReducer was called with state {} and action { type: '@@redux/PROBE_UNKNOWN_ACTION_9.r.k.r.i.c.n.m.i' }
// itemsReducer was called with state [] and action { type: '@@redux/INIT' }
// itemsReducer was called with state [] and action { type: '@@redux/PROBE_UNKNOWN_ACTION_4.f.i.z.l.3.7.s.y.v.i' }
var store_0 = createStore(reducer)
// 輸出:
// userReducer was called with state {} and action { type: '@@redux/INIT' }
// itemsReducer was called with state [] and action { type: '@@redux/INIT' }
// 正如你從輸出中看到的,每個 reducer 都被正確地調用了(但接收了個 init action @@redux/INIT )。
// 這個 action 是什么鬼?這是 combineReducers 實施的一次安全檢查,用以確保 reducer 永遠不會返回
// undefined。請注意,在 combineReducers 中第一次調用 init action 時,其實是隨機 action 來的,
// 但它們有個共同的目的 (即是做一個安全檢查)。
console.log('store_0 state after initialization:', store_0.getState())
// 輸出:
// store_0 state after initialization: { user: {}, items: [] }
// 有趣的是,我們發現 Redux 正確處理了 state 的各個部分。最終的 state 完全是一個簡單的對象,由
// userReducer 和 itemsReducer 返回的部分 state 共同組成。
// {
// user: {}, // {} is the slice returned by our userReducer
// items: [] // [] is the slice returned by our itemsReducer
// }
// 由於我們為每個 reducer 初始化了一個特殊的值(userReducer 的是空對象 {} ,itemsReducer 的是空數
// 組 [] ),所以在最終 Redux 的 state 中找到那些值並不是巧合。
// 現在,關於 reducer 如何工作我們已經有了清楚的理解。是時候去看看當 action 被分發(dispatch)時會對
// Redux 的 state 有什么影響。
// 章節 6 - dispatch-action.js
// 迄今為止我們的關注點都是綁定我們的 reducer,但我們還未 dispatch 任何一個 action。
// 我們將會用到上一章的 reducer ,並用它們處理一些 action:
var userReducer = function (state = {}, action) {
console.log('userReducer was called with state', state, 'and action', action)
switch (action.type) {
case 'SET_NAME':
return {
...state,
name: action.name
}
default:
return state;
}
}
var itemsReducer = function (state = [], action) {
console.log('itemsReducer was called with state', state, 'and action', action)
switch (action.type) {
case 'ADD_ITEM':
return [
...state,
action.item
]
default:
return state;
}
}
import { createStore, combineReducers } from 'redux'
var reducer = combineReducers({
user: userReducer,
items: itemsReducer
})
var store_0 = createStore(reducer)
console.log("\n", '### It starts here')
console.log('store_0 state after initialization:', store_0.getState())
// 輸出:
// store_0 state after initialization: { user: {}, items: [] }
// 讓我們來 dispatch 我們的第一個 action... 記住在 'simple-action-creator.js' 中所提到的:
// "為了 dispatch 一個 action,我們需要一個 dispatch 函數。"
// 我們所看到的 dispatch 函數,是 Redux 提供的,並且它會將 action 傳遞
// 給任何一個 reducer!dispatch 函數本質上是 Redux
// 的實例的屬性 "dispatch"
// dispatch 一個 action:
store_0.dispatch({
type: 'AN_ACTION'
})
// 輸出:
// userReducer was called with state {} and action { type: 'AN_ACTION' }
// itemsReducer was called with state [] and action { type: 'AN_ACTION' }
// 每一個 reducer 都被調用了,但是沒有一個 action type 是 reducer 需要的,
// 因此 state 是不會發生變化的:
console.log('store_0 state after action AN_ACTION:', store_0.getState())
// 輸出:store_0 state after action AN_ACTION: { user: {}, items: [] }
// 但是,等一下!我們是不是可以用一個 action creator 去發送一個 action?我們確實可以
// 用一個 actionCreator,但由於它只是返回一個 action,那么就意味着它不會攜帶任何東西
// 到這個例子中。但為了面對未來遇到的困難,我們還是以正確的方式,
// 即以 flux 理論去做吧。讓我們使用這個 action creator 發送一個我們想要的 action:
var setNameActionCreator = function (name) {
return {
type: 'SET_NAME',
name: name
}
}
store_0.dispatch(setNameActionCreator('bob'))
// 輸出:
// userReducer was called with state {} and action { type: 'SET_NAME', name: 'bob' }
// itemsReducer was called with state [] and action { type: 'SET_NAME', name: 'bob' }
console.log('store_0 state after action SET_NAME:', store_0.getState())
// 輸出:
// store_0 state after action SET_NAME: { user: { name: 'bob' }, items: [] }
// 我們剛剛處理了一個 action,並且它改變了應用的 state!
// 但是這似乎太簡單了,並且還不足以充當一個真實的用例。例如,
// 如果我們要在 dispatch action 之前做一些異步的操作,那應該怎么做呢?
// 我們將在下一章節 "dispatch-async-action.js" 中討論這個問題
// 至止,我們接觸的應用流程是這樣的:
// ActionCreator -> Action -> dispatcher -> reducer
// 章節 7 - dispatch-async-action-1.js
// 在上節教程中我們知道了如何分發 action 以及這些 action 如何通過 reducer 函數修改應用狀態。
// 但是,到目前為止,我們只考慮了一種情況,同步場景下的 action,准確地說是同步 action creator,它創建同步的 action,
// 也就是當 action creator 被調用時,action 會被立即返回。
// 我們來設想一個簡單的異步場景:
// 1)用戶點擊“Say Hi in 2 seconds”按鈕
// 2)當用戶點擊按鈕 A,我們希望經過兩秒,視圖顯示一條消息 Hi
// 3)兩秒過去之后,更新視圖,顯示消息 Hi
// 當然這條消息是應用的狀態之一,所以我們必然將其存儲於 Redux store。
// 但是我們希望的結果是,在調用 action creator 的兩秒之后才把消息存入 store(因為如果立即更新狀態,
// 那么就會立即觸發所有監聽狀態變更的訂閱者 —— 例如視圖,導致消息早於兩秒顯示)。
// 如果我們按照目前調用 action creator 的方式...
import { createStore, combineReducers } from 'redux'
var reducer = combineReducers({
speaker: function (state = {}, action) {
console.log('speaker was called with state', state, 'and action', action)
switch (action.type) {
case 'SAY':
return {
...state,
message: action.message
}
default:
return state;
}
}
})
var store_0 = createStore(reducer)
var sayActionCreator = function (message) {
return {
type: 'SAY',
message
}
}
console.log("\n", 'Running our normal action creator:', "\n")
console.log(new Date());
store_0.dispatch(sayActionCreator('Hi'))
console.log(new Date());
console.log('store_0 state after action SAY:', store_0.getState())
// 輸出(忽略初始輸出):
// Sun Aug 02 2015 01:03:05 GMT+0200 (CEST)
// speaker was called with state {} and action { type: 'SAY', message: 'Hi' }
// Sun Aug 02 2015 01:03:05 GMT+0200 (CEST)
// store_0 state after action SAY: { speaker: { message: 'Hi' } }
// ... 結果 store 被立即更新了。
// 我們希望看到的結果應該類似於下面這樣的代碼:
var asyncSayActionCreator_0 = function (message) {
setTimeout(function () {
return {
type: 'SAY',
message
}
}, 2000)
}
// 但是這樣 action creator 返回的不是 action 而是 undefined。所以這並不是我們所期望的解決方法。
// 這里有個訣竅:不返回 action,而是返回 function。這個 function 會在合適的時機 dispatch action。但是如果我們希望
// 這個 function 能夠 dispatch action,那么就需要向它傳入 dispatch 函數。於是代碼類似如下:
var asyncSayActionCreator_1 = function (message) {
return function (dispatch) {
setTimeout(function () {
dispatch({
type: 'SAY',
message
})
}, 2000)
}
}
// 你可能再次注意到 action creator 返回的不是 action 而是 function。
// 所以 reducer 函數很可能不知道如何處理這樣的返回值,而你也並不清楚是否可行,那么讓我們一起再做嘗試,一探究竟。
// 章節 8 - dispatch-async-action-2.js
// 運行之前我們在 dispatch-async-action-1.js 中實現的第一個異步 action creator:
import { createStore, combineReducers } from 'redux'
var reducer = combineReducers({
speaker: function (state = {}, action) {
console.log('speaker was called with state', state, 'and action', action)
switch (action.type) {
case 'SAY':
return {
...state,
message: action.message
}
default:
return state;
}
}
})
var store_0 = createStore(reducer)
var asyncSayActionCreator_1 = function (message) {
return function (dispatch) {
setTimeout(function () {
dispatch({
type: 'SAY',
message
})
}, 2000)
}
}
console.log("\n", 'Running our async action creator:', "\n")
store_0.dispatch(asyncSayActionCreator_1('Hi'))
// 輸出:
// ...
// /Users/classtar/Codes/redux-tutorial/node_modules/redux/node_modules/invariant/invariant.js:51
// throw error;
// ^
// Error: Invariant Violation: Actions must be plain objects. Use custom middleware for async actions.
// ...
// 我們所設計的 function 似乎沒有進入 reducer 函數。但是 Redux 給出了溫馨提示:使用自定義中間件(middleware)來支持異步 action。
// 看來我們的方向是正確的,可中間件(middleware)又是什么呢?
// 我向你保證 action creator asyncSayActionCreator_1 不僅沒有問題,而且只要我們搞清楚 middleware 的概念並掌握它的使用方法,
// 這個異步 action creator 就會按照我們所設想的結果執行。
// 開始下節教程:09_middleware.js
// 章節 9 - middleware.js
// 在 dispatch-async-action-2.js 章節中我們拋出了“中間件”的概念。中間件似乎
// 可以幫助我們處理異步 action。但中間件到底是什么呢?
// 通常來說中間件是在某個應用中 A 和 B 部分中間的那一塊,
// 中間件可以把 A 發送數據到 B 的形式從
// A -----> B
// 變成:
// A ---> middleware 1 ---> middleware 2 ---> middleware 3 --> ... ---> B
// 那么中間件在 Redux 中是如何工作的?
// 看上去 Redux 並不能自動處理 action creator 中返回的異步函數。
// 但如果在 action creator 和 reducer 之間增加一個中間件,就可以把這個函數轉成
// 適合 Redux 處理的內容:
// action ---> dispatcher ---> middleware 1 ---> middleware 2 ---> reducers
// 每當一個 action(或者其他諸如異步 action creator 中的某個函數)被分發時,
// 我們的中間件就會被調用
// 並且在需要的時候協助 action creator 分發真正的 action(或者什么都不做,
// 有時我們需要這么做)
// 在 Redux 中,中間件是純粹的函數,
// 有明確的使用方法並且嚴格的遵循以下格式:
/*
var anyMiddleware = function ({ dispatch, getState }) {
return function(next) {
return function (action) {
// 你的中間件業務相關代碼
}
}
}
*/
// 如上所述,中間件由三個嵌套的函數構成(會依次調用):
// 1) 第一層向其余兩層提供分發函數和 getState 函數
// (因為你的中間件或 action creator 可能需要從 state 中讀取數據)
// 2) 第二層提供 next 函數,它允許你顯式的將處理過的輸入傳遞給下一個中間件或 Redux
// (這樣 Redux 才能調用所有 reducer)。
// 3) 第三層提供從上一個中間件或從 dispatch 傳遞來的 action,
// 這個 action 可以調用下一個中間件(讓 action 繼續流動) 或者
// 以想要的方式處理 action。
// 學習過函數式編程的人可能會意識到給上述代碼提供了一個機會來使用
// 柯里化(如果你不理解也沒關系,跳過接下去的 10 行,不會影響你對 redux 的理解)。
// 使用柯里化,你可以簡化上述函數:
/*
// "curry" may come any functional programming library (lodash, ramda, etc.)
var thunkMiddleware = curry(
({dispatch, getState}, next, action) => (
// 你的中間件業務相關代碼
)
);
*/
// 我們為異步 action creator 提供的中間件叫 thunk middleware
// 它的代碼在:https://github.com/gaearon/redux-thunk.
// 它看上去是這樣 (為了可讀性使用 ES5 語法書寫該函數):
var thunkMiddleware = function ({ dispatch, getState }) {
// console.log('Enter thunkMiddleware');
return function(next) {
// console.log('Function "next" provided:', next);
return function (action) {
// console.log('Handling action:', action);
return typeof action === 'function' ?
action(dispatch, getState) :
next(action)
}
}
}
// 為了讓 Redux 知道我們有一個或多個中間件,我們使用 Redux 的
// 輔助函數:applyMiddleware.
// applyMiddleware 接收所有中間件作為參數,返回一個供 Redux createStore 調用的函數。
// 當最后這個函數被調用時,它會產生一個 Store 增強器,用來將所有中間件應用到 Store 的 dispatch 上。
// (來自 https://github.com/rackt/redux/blob/v1.0.0-rc/src/utils/applyMiddleware.js)
// 下面就是如何將一個中間件應用到 Redux store:
import { createStore, combineReducers, applyMiddleware } from 'redux'
const finalCreateStore = applyMiddleware(thunkMiddleware)(createStore)
// 針對多個中間件, 使用:applyMiddleware(middleware1, middleware2, ...)(createStore)
var reducer = combineReducers({
speaker: function (state = {}, action) {
console.log('speaker was called with state', state, 'and action', action)
switch (action.type) {
case 'SAY':
return {
...state,
message: action.message
}
default:
return state
}
}
})
const store_0 = finalCreateStore(reducer)
// 輸出:
// speaker was called with state {} and action { type: '@@redux/INIT' }
// speaker was called with state {} and action { type: '@@redux/PROBE_UNKNOWN_ACTION_s.b.4.z.a.x.a.j.o.r' }
// speaker was called with state {} and action { type: '@@redux/INIT' }
// 現在 store 的 middleware 已經准備好了,再來嘗試分發我們的異步 action:
var asyncSayActionCreator_1 = function (message) {
return function (dispatch) {
setTimeout(function () {
console.log(new Date(), 'Dispatch action now:')
dispatch({
type: 'SAY',
message
})
}, 2000)
}
}
console.log("\n", new Date(), 'Running our async action creator:', "\n")
store_0.dispatch(asyncSayActionCreator_1('Hi'))
// 輸出:
// Mon Aug 03 2015 00:01:20 GMT+0200 (CEST) Running our async action creator:
// Mon Aug 03 2015 00:01:22 GMT+0200 (CEST) 'Dispatch action now:'
// speaker was called with state {} and action { type: 'SAY', message: 'Hi' }
// 當我們調用異步 action creator 兩秒之后,action 成功被分發出去。
// 你可能會好奇,一個中間件如何 log 出所有已分發的 action ,
// 是這樣:
function logMiddleware ({ dispatch, getState }) {
return function(next) {
return function (action) {
console.log('logMiddleware action received:', action)
return next(action)
}
}
}
// 同樣的,下面是一個中間件,它會丟棄所有經過的 action(不是很實用,
// 但是如果加一些判斷就能實現丟棄一些 action,放到一些 action 給下一個中間件):
function discardMiddleware ({ dispatch, getState }) {
return function(next) {
return function (action) {
console.log('discardMiddleware action received:', action)
}
}
}
// 通過使用 logMiddleware 或 discardMiddleware 試着修改上述的 finalCreateStore 調用
// 看看會發生什么...
// 比如,這樣用:
// const finalCreateStore = applyMiddleware(discardMiddleware, thunkMiddleware)(createStore)
// 會讓你的 action 永遠無法到達 thunkMiddleware 和 reducer。
// 查看 http://rackt.org/redux/docs/introduction/Ecosystem.html 的中間件部分可以了解其他例子。
// 總結一下到目前為止我們所學的:
// 1) 我們知道怎樣寫 action 和 action creator
// 2) 我們知道怎樣分發 action
// 3) 我們知道怎樣使用中間件處理自定義 action,比如異步 action
// 對於 Flux 體系的完整閉環,我們還剩下唯一的一塊就是如何訂閱 state 的更新
// 並響應這些更新(比如重新渲染我們的組件)
// 所以我們怎么訂閱 Redux store 的更新呢?
// 繼續下一個教程: 10_state-subscriber.js
// 章節 10 - state-subscriber.js
// 我們接近完成一個完整的 Flux 閉環了,現在只差一個至關重要的環節:
// 沒有它,在 store 改變時我們就不能更新我們的視圖。
// 幸運的是,監視 Redux store 更新有一個很簡單的辦法:
/*
store.subscribe(function() {
// retrieve latest store state here
// Ex:
console.log(store.getState());
})
*/
// 是的,簡單到我們都開始重新相信聖誕老人了(譯者注:2333,對不起這個比喻太幽默了)
// 試一下這段代碼:
import { createStore, combineReducers } from 'redux'
var itemsReducer = function (state = [], action) {
console.log('itemsReducer was called with state', state, 'and action', action)
switch (action.type) {
case 'ADD_ITEM':
return [
...state,
action.item
]
default:
return state;
}
}
var reducer = combineReducers({ items: itemsReducer })
var store_0 = createStore(reducer)
store_0.subscribe(function() {
console.log('store_0 has been updated. Latest store state:', store_0.getState());
// 在這里更新你的視圖
})
var addItemActionCreator = function (item) {
return {
type: 'ADD_ITEM',
item: item
}
}
store_0.dispatch(addItemActionCreator({ id: 1234, description: 'anything' }))
// 輸出:
// ...
// store_0 has been updated. Latest store state: { items: [ { id: 1234, description: 'anything' } ] }
// 我們的訂閱回調成功的調用了,同時 store 現在包含了我們新增的條目。
// 理論上,到這就可以停止了。我們的 Flux loop 已經閉合,我們理解了構造 Flux 的全部概念,
// 實際上它也沒那么神秘。但是老實說,還有很多要講的,
// 為了讓最后一個概念保持簡單,
// 我們有意的在例子中去掉了一些東西:
// - 我們的訂閱回調沒有把 state 作為參數,為什么?
// - 既然我們沒有接受新的 state, 我們就被限定到了只能開發這個已經完成的 store (store_0) 所以這個辦法在
// 含有多個模塊的應用下不可行。
// - 我們究竟是怎么更新視圖的?
// - 怎么取消訂閱?
// - 更通俗的講,我們怎么把 Redux 和 React 結合到一起?
// 我們現在進入了一個“將 Redux 加入到 React” 的領域。
// 理解 Redux 可以無條件綁定到 React 上是很重要的。
// Redux 是一個“為 Javascript 應用而生的可預測的狀態容器”,
// 你有很多方式去使用它,而 React 應用只不過是其中一個。
// 從這個角度看,如果沒有 react-redux (https://github.com/reactjs/react-redux),我們將失去很多。
// 在 Redux 1.0.0 之前它是包含在 Redux 中的,這個庫節省了我們很多時間,
// 它包含了在 React 中使用 Redux 時所有的綁定。
// 回到訂閱這件事,為什么我們這個訂閱函數看上去非常簡單
// 而且沒有提供很多特性?
// 這就是 Redux 精彩之處了! 它所有 API 都很抽象(包括訂閱),
// 支持高度擴展,允許開發者造出一些瘋狂的輪子
// 比如 Redux DevTools (https://github.com/gaearon/redux-devtools).
// 但是最后我們還是需要一個更好的接口訂閱我們的 store 變化。這也就是 react-redux 給帶給我們的:
// 一個完美填補原生 Redux 訂閱機制和開發者的期待之間的空缺的 API ,
// 這樣我們不再需要直接使用訂閱。而只是
// 使用 “provide” 和 “connect” 綁定,不必再關心隱含在內的訂閱方法。
// 所以,訂閱方法依然會被我們使用,
// 只不過它通過高度整合的接口替我們處理 redux state 的連接。
// 現在我們隱藏了那些綁定,並且展示了連接你的組件和 Redux 的 state 是很輕松的一件事。
// 繼續下一個教程: 11_Provider-and-connect.js
// 章節 11 - Provider-and-connect.js
// 這其實是教程的最后一章,一起聊聊如何把 Redux 和 React 綁定在一起。
// 要運行下面的示例,你需要一個瀏覽器。
// 本示例中的代碼和注釋都在 ./11_src/src/ 目錄下。
// 當你讀到下面這段話的時間,請運行 11_src/src/server.js。
// 開發一個 React 應用和服務器來讓瀏覽器可以訪問,我們會用到:
// - 用 node HTTP(https://nodejs.org/api/http.html) 創建一個非常簡單的服務器
// - 用 Webpack 去打包我們的應用,
// - 神奇的 Webpack Dev Server (http://webpack.github.io/docs/webpack-dev-server.html)
// 作為一個專門的 node 服務器,並監聽 JS 改變自動編譯
// - 超棒的 React Hot Loader http://gaearon.github.io/react-hot-loader/ (Dan Abramov
// 開發的另一個很棒的項目,沒錯,他就是 Redux 的作者) ,提供非常棒的
// DX (開發體驗) ,當我們在編輯器中修改代碼時,
// 在瀏覽器中可以熱加載來顯示效果。
// 提醒一下正在使用 React 的開發者:本應用是基於 React 0.14 構建的
// 我不想在這里詳細地解釋如何設置 Webpack Dev Server 和 React Hot Loader,
// 因為在 React Hot Loader 的文檔中已經說的很好了。
import webpackDevServer from './11_src/src/webpack-dev-server'
// 我們應用啟動的主要服務器請求都是來自這個文件。
import server from './11_src/src/server'
// 如果 5050 端口號已經被占用了,那么就修改下面的端口號。
// 如果端口號是 X,那么我們可以用 X 作為服務器的端口號,用 X+1 作為 webpack-dev-server 的端口號
const port = 5050
// 啟動 webpack dev server...
webpackDevServer.listen(port)
// ... 還有主應用服務器。
server.listen(port)
console.log(`Server is listening on http://127.0.0.1:${port}`)
// 轉到 11_src/src/server.js...
https://github.com/react-guide/redux-tutorial-cn