redux摘要2
Action 發出以後,Reducer 立即算出 State,這叫做同步;Action 發出以後,過一段時間再執行 Reducer,這就是非同步
怎麼才能 Reducer 在非同步操作結束後自動執行呢?這就要用到新的工具:中介軟體(middleware)
中介軟體的概念
為了理解中介軟體,讓我們站在框架作者的角度思考問題:如果要新增功能,你會在哪個環節新增?
(1)Reducer:純函式,只承擔計算 State 的功能,不合適承擔其他功能,也承擔不了,因為理論上,純函式不能進行讀寫操作。
(2)View:與 State 一一對應,可以看作 State 的視覺層,也不合適承擔其他功能。
(3)Action:存放資料的物件,即訊息的載體,只能被別人操作,自己不能進行任何操作。
想來想去,只有傳送 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進行了重定義,在傳送 Action 前後新增了列印功能。這就是中介軟體的雛形。
中介軟體就是一個函式,對store.dispatch方法進行了改造,在發出 Action 和執行 Reducer 這兩步之間,新增了其他功能。
中介軟體的用法
本教程不涉及如何編寫中介軟體,因為常用的中介軟體都有現成的,只要引用別人寫好的模組即可。比如,上一節的日誌中介軟體,就有現成的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()的功能增強。
這裡有兩點需要注意:
(1)createStore方法可以接受整個應用的初始狀態作為引數,那樣的話,applyMiddleware就是第三個引數了。
const store = createStore(
reducer,
initial_state,
applyMiddleware(logger)
);
(2)中介軟體的次序有講究。
const store = createStore(
reducer,
applyMiddleware(thunk, promise, logger)
);
上面程式碼中,applyMiddleware方法的三個引數,就是三個中介軟體。有的中介軟體有次序要求,使用前要查一下文件。比如,logger就一定要放在最後,否則輸出結果會不正確
applyMiddlewares()
看到這裡,你可能會問,applyMiddlewares這個方法到底是幹什麼的?
它是 Redux 的原生方法,作用是將所有中介軟體組成一個陣列,依次執行。下面是它的原始碼
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}
}
}
上面程式碼中,所有中介軟體被放進了一個陣列chain,然後巢狀執行,最後執行store.dispatch。可以看到,中介軟體內部(middlewareAPI)可以拿到getState和dispatch這兩個方法。
非同步操作的基本思路
理解了中介軟體以後,就可以處理非同步操作了。
同步操作只要發出一種 Action 即可,非同步操作的差別是它要發出三種 Action。
操作發起時的 Action
操作成功時的 Action
操作失敗時的 Action
以向伺服器取出資料為例,三種 Action 可以有兩種不同的寫法。
// 寫法一:名稱相同,引數不同
{ 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 也要進行改造,反映不同的操作狀態。下面是 State 的一個例子
let state = {
// ...
isFetching: true,
didInvalidate: true,
lastUpdated: 'xxxxxxx'
};
上面程式碼中,State 的屬性isFetching表示是否在抓取資料。didInvalidate表示資料是否過時,lastUpdated表示上一次更新時間。
現在,整個非同步操作的思路就很清楚了。
操作開始時,送出一個 Action,觸發 State 更新為"正在操作"狀態,View 重新渲染
操作結束後,再送出一個 Action,觸發 State 更新為"操作結束"狀態,View 再一次重新渲染
redux-thunk 中介軟體
非同步操作至少要送出兩個 Action:使用者觸發第一個 Action,這個跟同步操作一樣,沒有問題;如何才能在操作結束時,系統自動送出第二個 Action 呢?
奧妙就在 Action Creator 之中。
class AsyncApp extends Component {
componentDidMount() {
const { dispatch, selectedPost } = this.props
dispatch(fetchPosts(selectedPost))
}
上面程式碼是一個非同步元件的例子。載入成功後(componentDidMount方法),它送出了(dispatch方法)一個 Action,向伺服器要求資料 fetchPosts(selectedSubreddit)。這裡的fetchPosts就是 Action Creator。
下面就是fetchPosts的程式碼,關鍵之處就在裡面。
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(動作生成器),返回一個函式。這個函式執行後,先發出一個Action(requestPosts(postTitle)),然後進行非同步操作。拿到結果後,先將結果轉成 JSON 格式,然後再發出一個 Action( receivePosts(postTitle, json))。
上面程式碼中,有幾個地方需要注意。
(1)fetchPosts返回了一個函式,而普通的 Action Creator 預設返回一個物件。
(2)返回的函式的引數是dispatch和getState這兩個 Redux 方法,普通的 Action Creator 的引數是 Action 的內容。
(3)在返回的函式之中,先發出一個 Action(requestPosts(postTitle)),表示操作開始。
(4)非同步操作結束之後,再發出一個 Action(receivePosts(postTitle, json)),表示操作結束。
這樣的處理,就解決了自動傳送第二個 Action 的問題。但是,又帶來了一個新的問題,Action 是由store.dispatch
方法傳送的。而store.dispatch
方法正常情況下,引數只能是物件,不能是函式。
這時,就要使用中介軟體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,使得後者可以接受函式作為引數。
因此,非同步操作的第一種解決方案就是,寫出一個返回函式的 Action Creator,然後使用redux-thunk中介軟體改造store.dispatch。
redux-promise 中介軟體
既然 Action Creator 可以返回函式,當然也可以返回其他值。另一種非同步操作的解決方案,就是讓 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 物件作為引數。這時,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
// 發出同步 Action
dispatch(requestPosts(selectedPost));
// 發出非同步 Action
dispatch(createAction(
'FETCH_POSTS',
fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
));
}
上面程式碼中,第二個dispatch方法發出的是非同步 Action,只有等到操作結束,這個 Action 才會實際發出。注意,createAction的第二個引數必須是一個 Promise 物件。
React-Redux 的用法
為了方便使用,Redux 的作者封裝了一個 React 專用的庫 React-Redux,本文主要介紹它。
這個庫是可以選用的。實際專案中,你應該權衡一下,是直接使用 Redux,還是使用 React-Redux。後者雖然提供了便利,但是需要掌握額外的 API,並且要遵守它的元件拆分規範。
UI 元件
React-Redux 將所有元件分成兩大類:UI 元件(presentational component)和容器元件(container component)
UI 元件有以下幾個特徵。
只負責 UI 的呈現,不帶有任何業務邏輯
沒有狀態(即不使用this.state這個變數)
所有資料都由引數(this.props)提供
不使用任何 Redux 的 API
下面就是一個 UI 元件的例子。
const Title =
value => <h1>{value}</h1>;
因為不含有狀態,UI 元件又稱為"純元件",即它純函式一樣,純粹由引數決定它的值。
二、容器元件
容器元件的特徵恰恰相反。
負責管理資料和業務邏輯,不負責 UI 的呈現
帶有內部狀態
使用 Redux 的 API
總之,只要記住一句話就可以了:UI 元件負責 UI 的呈現,容器元件負責管理資料和邏輯。
你可能會問,如果一個元件既有 UI 又有業務邏輯,那怎麼辦?回答是,將它拆分成下面的結構:外面是一個容器元件,裡面包了一個UI 元件。前者負責與外部的通訊,將資料傳給後者,由後者渲染出檢視。
React-Redux 規定,所有的 UI 元件都由使用者提供,容器元件則是由 React-Redux 自動生成。也就是說,使用者負責視覺層,狀態管理則是全部交給它。
connect()
React-Redux 提供connect方法,用於從 UI 元件生成容器元件。connect的意思,就是將這兩種元件連起來。
import { connect } from 'react-redux'
const VisibleTodoList = connect()(TodoList);
上面程式碼中,TodoList是 UI 元件,VisibleTodoList就是由 React-Redux 通過connect方法自動生成的容器元件。
但是,因為沒有定義業務邏輯,上面這個容器元件毫無意義,只是 UI 元件的一個單純的包裝層。為了定義業務邏輯,需要給出下面兩方面的資訊。
(1)輸入邏輯:外部的資料(即state物件)如何轉換為 UI 元件的引數
(2)輸出邏輯:使用者發出的動作如何變為 Action 物件,從 UI 元件傳出去。
因此,connect方法的完整 API 如下。
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
上面程式碼中,connect方法接受兩個引數:mapStateToProps和mapDispatchToProps。它們定義了 UI 元件的業務邏輯。前者負責輸入邏輯,即將state對映到 UI 元件的引數(props),後者負責輸出邏輯,即將使用者對 UI 元件的操作對映成 Action。
mapStateToProps()
mapStateToProps是一個函式。它的作用就是像它的名字那樣,建立一個從(外部的)state物件到(UI 元件的)props物件的對映關係。
作為函式,mapStateToProps執行後應該返回一個物件,裡面的每一個鍵值對就是一個對映。請看下面的例子。
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
上面程式碼中,mapStateToProps是一個函式,它接受state作為引數,返回一個物件。這個物件有一個todos屬性,代表 UI 元件的同名引數,後面的getVisibleTodos也是一個函式,可以從state算出 todos 的值。
下面就是getVisibleTodos的一個例子,用來算出todos。
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
default:
throw new Error('Unknown filter: ' + filter)
}
}
mapStateToProps會訂閱 Store,每當state更新的時候,就會自動執行,重新計算 UI 元件的引數,從而觸發 UI 元件的重新渲染。
mapStateToProps的第一個引數總是state物件,還可以使用第二個引數,代表容器元件的props物件。
// 容器元件的程式碼
// <FilterLink filter="SHOW_ALL">
// All
// </FilterLink>
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}
使用ownProps作為引數後,如果容器元件的引數發生變化,也會引發 UI 元件重新渲染。
connect方法可以省略mapStateToProps引數,那樣的話,UI 元件就不會訂閱Store,就是說 Store 的更新不會引起 UI 元件的更新。
mapDispatchToProps()
mapDispatchToProps是connect函式的第二個引數,用來建立 UI 元件的引數到store.dispatch方法的對映。也就是說,它定義了哪些使用者的操作應該當作 Action,傳給 Store。它可以是一個函式,也可以是一個物件。
如果mapDispatchToProps是一個函式,會得到dispatch和ownProps(容器元件的props物件)兩個引數。
const mapDispatchToProps = (
dispatch,
ownProps
) => {
return {
onClick: () => {
dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: ownProps.filter
});
}
};
}
從上面程式碼可以看到,mapDispatchToProps作為函式,應該返回一個物件,該物件的每個鍵值對都是一個對映,定義了 UI 元件的引數怎樣發出 Action。
如果mapDispatchToProps是一個物件,它的每個鍵名也是對應 UI 元件的同名引數,鍵值應該是一個函式,會被當作 Action creator ,返回的 Action 會由 Redux 自動發出。舉例來說,上面的mapDispatchToProps寫成物件就是下面這樣。
const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}
<Provider> 元件
connect方法生成容器元件以後,需要讓容器元件拿到state物件,才能生成 UI 元件的引數。
一種解決方法是將state物件作為引數,傳入容器元件。但是,這樣做比較麻煩,尤其是容器元件可能在很深的層級,一級級將state傳下去就很麻煩。
React-Redux 提供Provider元件,可以讓容器元件拿到state。
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
上面程式碼中,Provider
在根元件外面包了一層,這樣一來,App
的所有子元件就預設都可以拿到state
了。
它的原理是React
元件的context
屬性,請看原始碼。
例項:計數器
我們來看一個例項。下面是一個計數器元件,它是一個純的 UI 元件。
class Counter extends Component {
render() {
const { value, onIncreaseClick } = this.props
return (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
)
}
}
上面程式碼中,這個 UI 元件有兩個引數:value和onIncreaseClick。前者需要從state計算得到,後者需要向外發出 Action。
接著,定義value到state的對映,以及onIncreaseClick到dispatch的對映。
function mapStateToProps(state) {
return {
value: state.count
}
}
function mapDispatchToProps(dispatch) {
return {
onIncreaseClick: () => dispatch(increaseAction)
}
}
// Action Creator
const increaseAction = { type: 'increase' }
然後,使用connect方法生成容器元件。
const App = connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
然後,定義這個元件的 Reducer。
// Reducer
function counter(state = { count: 0 }, action) {
const count = state.count
switch (action.type) {
case 'increase':
return { count: count + 1 }
default:
return state
}
}
最後,生成store
物件,並使用Provider
在根元件外面包一層。
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider, connect } from 'react-redux'
// React component
class Counter extends Component {
render() {
const { value, onIncreaseClick } = this.props
return (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
)
}
}
Counter.propTypes = {
value: PropTypes.number.isRequired,
onIncreaseClick: PropTypes.func.isRequired
}
// Action
const increaseAction = { type: 'increase' }
// Reducer
function counter(state = { count: 0 }, action) {
const count = state.count
switch (action.type) {
case 'increase':
return { count: count + 1 }
default:
return state
}
}
// Store
const store = createStore(counter)
// Map Redux state to component props
function mapStateToProps(state) {
return {
value: state.count
}
}
// Map Redux actions to component props
function mapDispatchToProps(dispatch) {
return {
onIncreaseClick: () => dispatch(increaseAction)
}
}
// Connected Component
const App = connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
React-Router 路由庫
使用React-Router的專案,與其他專案沒有不同之處,也是使用Provider在Router外面包一層,畢竟Provider的唯一功能就是傳入store物件。
const Root = ({ store }) => (
<Provider store={store}>
<Router>
<Route path="/" component={App} />
</Router>
</Provider>
);
相關文章
- 《HTTP/2 基礎教程》 閱讀摘要HTTP
- 《Redis入門指南(第2版)》摘要Redis
- Redux原始碼分析(2) - createStoreRedux原始碼
- 控制元件測試功能點摘要2控制元件
- 【文末福利】功能摘要 | Premiere Pro(2023 年 2 月更新)REM
- 小菜鳥的React之路--Redux基礎2ReactRedux
- 運維摘要運維
- 文字自動摘要:基於TextRank的中文新聞摘要
- 文字摘要簡述
- 摘要商城總結
- 「xDeepFM」- 論文摘要
- ReduxRedux
- [譯] 使用 React、Redux 和 SVG 開發遊戲 - Part 2ReactReduxSVG開發遊戲
- redux && react-redux原始碼解析ReduxReact原始碼
- 文字摘要論文列表
- 《圖解 HTTP》 摘要一圖解HTTP
- Digest Auth 摘要認證
- 【筆記摘要】前端效能筆記前端
- 資訊摘要技術
- Vuex、Flux、Redux、Redux-saga、Dva、MobXVueRedux
- redux中介軟體之redux-thunkRedux
- React 入門-redux 和 react-reduxReactRedux
- 深入理解redux之從redux原始碼到react-redux的原理Redux原始碼React
- redux原理Redux
- Redux reselectRedux
- 巧用ReduxRedux
- Redux with HooksReduxHook
- 乾貨|當深度學習遇見自動文字摘要,seq2seq+attention深度學習
- react-redux/redux相關API,用法原理ReactReduxAPI
- 對React、Redux、React-Redux詳細剖析ReactRedux
- 帶你瞭解redux與react-reduxReduxReact
- UI2CODE再進化!結合Redux的框架升級!UIRedux框架
- css佈局知識摘要CSS
- 【譯】WebSocket協議——摘要(Abstract)Web協議
- JS祕密花園摘要JS
- 「Attentional Factorization Machines」- 論文摘要Mac
- compare用法示例•選項摘要
- 專利說明書摘要