Redux
Redux 是一個面向 JavaScript 應用的狀態管理工具。它可以幫助我們寫出更清晰,更容易測試的程式碼,並且可以使用在任何不同的環境下。Redux 是 Flux 的一種實現,它簡化了 Flux 繁瑣的 store 而採用單一資料來源的方式,大大減小了狀態管理的複雜度。相比 Flux 更容易被大家接受。
你可以在 React 中使用。與其它 JavaScript 一起使用也是可以的。它是與框架無關的。
Note: 面試送命題,被問到 Vuex 和 Redux 哪個好?這個真的是送命題,尤其是遇到那種主觀技術傾向嚴重的面試官。比如偏好 Vue 或者 React 的,畢竟 Redux 的用法繁瑣,需要多寫很多程式碼而被國人所詬病。但是很多人卻沒有看到 Redux 使程式碼結構更清晰。 Note: 之前發表在掘金的 Redux 原始碼解析。
Redux 包含 reducers,middleware,store enhancers,但是卻非常簡單。如果你之前構建過 Flux 應用,那麼對於你來說就更簡單了。即使你沒有使用過 Flux,依然也是很簡單的。
Actions
Actions 是應用程式將資料傳送到 store 的載體。可以通過 store.dispatch 來將 action 傳送到 store 中。
下面是幾個例子:
const ADD_TODO = 'ADD_TODO';
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
複製程式碼
Actions 是一個原生 JavaScript 物件,並且必須帶有一個 type 屬性作為識別行為的標示。type 是一個靜態字串。如果你的應用很龐大,那麼你需要把他們移到一個模組中:
import { ADD_TODO, REMOVE_TODO } from '../actionTypes'
複製程式碼
Action Creators
Action Creators 是用於建立 action 物件的函式:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
複製程式碼
在一些複雜應用中,我們還需要在 Action Creator 中派發其它 action:
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text
}
dispatch(action)
}
複製程式碼
當然還有更復雜的,直接派發一個 Action Creator:
dispatch(addTodo(text))
dispatch(completeTodo(index))
複製程式碼
Reducers
Reducers 用於根據接受的 action 物件,對 store 內的資料進行相應的處理,action 只描述發生了什麼,並不描述應用程式的狀態改變,改變發生在 reducer 中。
在 Redux 中,所有的狀態儲存在一個單一物件中,在某些複雜的應用中,你需要設計複雜的實體。我們建議你保持你的狀態儘可能普通,不要巢狀。保持每一個實體和一個 ID 這樣的 key 關聯。然後用 ID 在其它實體中訪問這個實體。把應用的狀態想象成資料庫。
Reducer 必須是一個純函式,它的引數是之前的狀態和接收的 action,然後返回一個新的狀態物件。
(previousState, action) => newState
複製程式碼
之所以叫做 reducer 是因為它被作為一種函式被傳入到 Array.prototype.reduce(reducer, ?initialValue)。這是保持 reducer 是一個純函式是非常重要的。不要在裡面做下面的事情:
- 改變引數
- 執行 API 請求,或者路由切換
- 呼叫非純函式,比如 Date.now()
下面的程式碼將會是一個非常簡單的 reducer 實現:
import { VisibilityFilters } from './actions'
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
}
function todoApp(state, action) {
if (typeof state === 'undefined') {
return initialState
}
// For now, don't handle any actions
// and just return the state given to us.
return state
}
複製程式碼
我們必須在 reducer 中處理完 action 後建立一個新的 state 並作為返回值。像下面這樣:
{ ...state, ...newState }
複製程式碼
reducer 在預設情況下或者遇到未知 action 的時候,需要返回傳入的 state 。
import {
ADD_TODO,
TOGGLE_TODO,
SET_VISIBILITY_FILTER,
VisibilityFilters
} from './actions'
...
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})
default:
return state
}
}
複製程式碼
就像之前提到的,我們並不是直接操作 state 或者它的屬性,而是返回一個新的物件。
有的時候,我們的系統過於龐大,這樣 reducer 就會變得複雜而龐大。這個時候我們就需要將 reducer 拆分
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
複製程式碼
每一個 reducer 都只管理屬於自己那部分狀態。而每一個 reducer 返回的狀態都會成為 store 的一部分。這裡我們需要通過 combineReducers() 來將這些 reducer 組合到一起
import { combineReducers } from 'redux'
const todoApp = combineReducers({
visibilityFilter,
todos
})
export default todoApp
複製程式碼
Store
Store 就是一堆物件的集合。Store 包含以下功能:
- 保持應用中的狀態
- 允許通過 getState 訪問狀態
- 允許通過 dispatch 更新狀態
- 註冊訂閱者
- 取消註冊的訂閱者
import { createStore } from 'redux'
import todoApp from './reducers'
const store = createStore(todoApp)
複製程式碼
createStore 具有一個可選引數,可以初始化 store 中的狀態。這對於部分場景很重要,比如說內建入後端預先處理的資料,直接注入到 store 中,這樣頁面就避免了 ajax 請求的響應時間提升了頁面顯示速度,如果沒有 SEO 要求的話,這種方式是一個成本非常低的提高首屏載入速度的方式,之前我在專案中使用過。
const store = createStore(todoApp, window.STATE_FROM_SERVER)
複製程式碼
我們可以通過 dispatch 派發 action 物件來改變 store 內部儲存的狀態:
import {
addTodo,
toggleTodo,
setVisibilityFilter,
VisibilityFilters
} from './actions'
// Log the initial state
console.log(store.getState())
// Every time the state changes, log it
// Note that subscribe() returns a function for unregistering the listener
const unsubscribe = store.subscribe(() =>
console.log(store.getState())
)
// Dispatch some actions
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
// Stop listening to state updates
unsubscribe()
複製程式碼
Redux 資料流
Redux 遵循嚴格的單向資料流。意味著所有的應用都要遵循相同邏輯來管理狀態,也正因如此,程式碼變得更加清晰,易於維護。並且由於採用單一資料來源。避免了 Flux 複雜而難以管理狀態的問題。但是,會讓開發人員覺得繁瑣。需要定義非常多的 action 和 reducer。
基於 Redux 的應用中,資料的生命週期要遵循一下幾步:
- 通過 dispatch 派發 action 物件
- store 執行通過 combineReducers 註冊的 reducer,根據 action 的 type 做對應的狀態更新
- 通過 combineReducers 組合的 reducers 將所有 reducer 返回的狀態集中到一個狀態樹中
- store 將返回的新狀態樹儲存起來
非同步 action
當我們使用一個非同步 api 的時候,一般會有兩個階段:發起請求,收到迴應。
這兩個階段通常會更新應用的狀態,因此你需要 dispatch 的 action 被同步處理。通常,對於 API 請求你希望 dispatch 三個不同的 action:
- 一個用於告訴 reducer 請求開始的 action (通常會設定一個 isFetching 標誌告知 UI 需要顯示一個載入動畫)
- 一個用於告訴 reducer 請求成功的 action (這裡我們需要將接收到的資料更新到 store 中,並重置 isFetching)
- 一個用於告訴 reducer 請求異常的 action (重置 isFetching,更新 store 中一個可以通知 UI 發生錯誤的狀態)
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }
複製程式碼
通常,我們需要在非同步開始前和回撥中通過 store.dispatch 來派發這些 action 來告知 store 更新狀態。
Note: 這裡要注意 action 派發的順序。因為非同步的返回時間是無法確定的。所以我們需要藉助 Promise 或者 async/await Generator 來控制非同步流,保證 dispatch 的 action 有一個合理的順序。
同步 action
對於同步 action,我們只需要在 action creator 中返回一個 action 純物件即可。
export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'
export function selectSubreddit(subreddit) {
return {
type: SELECT_SUBREDDIT,
subreddit
}
}
複製程式碼
Async Flow
Redux 僅支援同步的資料流,只能在中介軟體中處理非同步。因此我們需要在 中介軟體中才能處理非同步的資料流。
Redux-Thunk 是一個非常好的非同步 action 處理中介軟體,可以幫我們處理非同步 action 更加方便和清晰。
下面是一個通過 Redux-Thunk 處理非同步 action 的例子:
import fetch from 'cross-fetch'
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, // lets us dispatch() functions
loggerMiddleware // neat middleware that logs actions
)
)
store.dispatch(selectSubreddit('reactjs'))
store
.dispatch(fetchPosts('reactjs'))
.then(() => console.log(store.getState()))
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
}
}
// Meet our first thunk action creator!
// Though its insides are different, you would use it just like any other action creator:
// store.dispatch(fetchPosts('reactjs'))
export function fetchPosts(subreddit) {
// Thunk middleware knows how to handle functions.
// It passes the dispatch method as an argument to the function,
// thus making it able to dispatch actions itself.
return function (dispatch) {
// First dispatch: the app state is updated to inform
// that the API call is starting.
dispatch(requestPosts(subreddit))
// The function called by the thunk middleware can return a value,
// that is passed on as the return value of the dispatch method.
// In this case, we return a promise to wait for.
// This is not required by thunk middleware, but it is convenient for us.
return fetch(`https://www.reddit.com/r/\${subreddit}.json`)
.then(
response => response.json(),
// Do not use catch, because that will also catch
// any errors in the dispatch and resulting render,
// causing a loop of 'Unexpected batch number' errors.
// https://github.com/facebook/react/issues/6895
error => console.log('An error occurred.', error)
)
.then(json =>
// We can dispatch many times!
// Here, we update the app state with the results of the API call.
dispatch(receivePosts(subreddit, json))
)
}
}
複製程式碼
中介軟體
在前面,我們看到,我們可以通過中介軟體來完成非同步 action 處理。如果你使用過 express 或者 koa,那麼就更容易理解中介軟體。中介軟體就是一些程式碼,會在接收到請求的時候作出迴應。
Redux 的中介軟體解決的是和 express 或者 koa 完全不同的問題,但是原理上差不多。它提供一種第三方外掛機制,來在 dispatch 和 reducer 之間做一些特殊處理。就像下面這樣:
const next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
複製程式碼
那麼我們如何完成一個自己的中介軟體呢?下面是一個典型的例子:
// 其中 next 就是 dispatch
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
const crashReporter = store => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}
// 通過 appliMiddleware 來註冊自己的中介軟體
import { createStore, combineReducers, applyMiddleware } from 'redux'
const todoApp = combineReducers(reducers)
const store = createStore(
todoApp,
// applyMiddleware() tells createStore() how to handle middleware
applyMiddleware(logger, crashReporter)
)
複製程式碼