Redux 是一個面向 JavaScript 應用的狀態管理工具。它能夠幫助咱們寫出更清晰,更容易測試的代碼,而且可使用在任何不一樣的環境下。Redux 是 Flux 的一種實現,它簡化了 Flux 繁瑣的 store 而採用單一數據源的方式,大大減少了狀態管理的複雜度。相比 Flux 更容易被你們接受。react
你能夠在 React 中使用。與其它 JavaScript 一塊兒使用也是能夠的。它是與框架無關的。git
Note: 面試送命題,被問到 Vuex 和 Redux 哪一個好?這個真的是送命題,尤爲是遇到那種主觀技術傾向嚴重的面試官。好比偏好 Vue 或者 React 的,畢竟 Redux 的用法繁瑣,須要多寫不少代碼而被國人所詬病。可是不少人卻沒有看到 Redux 使代碼結構更清晰。 Note: 以前發表在掘金的 Redux 源碼解析。github
Redux 包含 reducers,middleware,store enhancers,可是卻很是簡單。若是你以前構建過 Flux 應用,那麼對於你來講就更簡單了。即便你沒有使用過 Flux,依然也是很簡單的。面試
Actions 是應用程序將數據發送到 store 的載體。能夠經過 store.dispatch 來將 action 發送到 store 中。ajax
下面是幾個例子:數據庫
const ADD_TODO = 'ADD_TODO';
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
複製代碼
Actions 是一個原生 JavaScript 對象,而且必須帶有一個 type 屬性做爲識別行爲的標示。type 是一個靜態字符串。若是你的應用很龐大,那麼你須要把他們移到一個模塊中:express
import { ADD_TODO, REMOVE_TODO } from '../actionTypes'
複製代碼
Action Creators 是用於建立 action 對象的函數:json
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
複製代碼
在一些複雜應用中,咱們還須要在 Action Creator 中派發其它 action:redux
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text
}
dispatch(action)
}
複製代碼
固然還有更復雜的,直接派發一個 Action Creator:後端
dispatch(addTodo(text))
dispatch(completeTodo(index))
複製代碼
Reducers 用於根據接受的 action 對象,對 store 內的數據進行相應的處理,action 只描述發生了什麼,並不描述應用程序的狀態改變,改變發生在 reducer 中。
在 Redux 中,全部的狀態存儲在一個單一對象中,在某些複雜的應用中,你須要設計複雜的實體。咱們建議你保持你的狀態儘量普通,不要嵌套。保持每個實體和一個 ID 這樣的 key 關聯。而後用 ID 在其它實體中訪問這個實體。把應用的狀態想象成數據庫。
Reducer 必須是一個純函數,它的參數是以前的狀態和接收的 action,而後返回一個新的狀態對象。
(previousState, action) => newState
複製代碼
之因此叫作 reducer 是由於它被做爲一種函數被傳入到 Array.prototype.reduce(reducer, ?initialValue)。這是保持 reducer 是一個純函數是很是重要的。不要在裏面作下面的事情:
下面的代碼將會是一個很是簡單的 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 包含如下功能:
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 遵循嚴格的單向數據流。意味着全部的應用都要遵循相同邏輯來管理狀態,也正因如此,代碼變得更加清晰,易於維護。而且因爲採用單一數據源。避免了 Flux 複雜而難以管理狀態的問題。可是,會讓開發人員以爲繁瑣。須要定義很是多的 action 和 reducer。
基於 Redux 的應用中,數據的生命週期要遵循一下幾步:
當咱們使用一個異步 api 的時候,通常會有兩個階段:發起請求,收到迴應。
這兩個階段一般會更新應用的狀態,所以你須要 dispatch 的 action 被同步處理。一般,對於 API 請求你但願 dispatch 三個不一樣的 action:
{ 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 creator 中返回一個 action 純對象便可。
export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'
export function selectSubreddit(subreddit) {
return {
type: SELECT_SUBREDDIT,
subreddit
}
}
複製代碼
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)
)
複製代碼