Redux這個npm包继控,提供若干API讓我們使用reducer創(chuàng)建store抵代,并能更新store中的數(shù)據(jù)或獲取store中最新的狀態(tài)描沟〗┐常“Redux應(yīng)用”指使用redux結(jié)合視圖層實(shí)現(xiàn)(React)及其他前端應(yīng)用必備組件(路由庫泌类、Ajax請求庫)組成的完成類Flux思想的前端應(yīng)用癞谒。
Redux 三大原則
單一數(shù)據(jù)源
Redux思想中,一個應(yīng)用永遠(yuǎn)只有唯一的數(shù)據(jù)源刃榨。combineReducers化解了數(shù)據(jù)源對象過于龐大的問題弹砚。狀態(tài)是只讀的
Redux中,我們并不會自己用代碼來定義一個store枢希。取而代之的是桌吃,我們定義一個rducer,它的功能是根據(jù)當(dāng)前觸發(fā)的action對當(dāng)前應(yīng)用的狀態(tài)(state)進(jìn)行迭代苞轿,這里我們斌沒有直接修改應(yīng)用的狀態(tài)茅诱,而是返回一份全新的狀態(tài)。
Redux提供的createStore方法會根據(jù)reducer生成store搬卒。最后瑟俭,我們利用 store.dispatch 方法來達(dá)到修改狀態(tài)的目的。狀態(tài)修改均有純函數(shù)完成
在Redux中契邀,我們通過定義reducer來確定狀態(tài)的修改摆寄。
Redux核心API
Redux的核心是一個store,這個store由Redux提供的 createStore(reducers, [,initialState])方法生成坯门。
Redux里微饥,負(fù)責(zé)響應(yīng)action并修改數(shù)據(jù)的角色是reducer。其函數(shù)簽名為reducer(previousState, action ) => newState
古戴。所以欠橘,reducer的職責(zé)就是根據(jù) previousState 和 action 計算出新的 newState。
// MyReducer.js
const initialState = {
todos: [],
}
// 我們定義的todos這個reducer在第一次執(zhí)行的時候现恼,會返回 { todos: [] }作為初始化狀態(tài)
function todos(previousState = initalState, action) {
switch(action.type) {
case 'xx': {
// 具體的業(yè)務(wù)邏輯
}
default:
return previousState;
}
}
Redux = Reducer + Flux
通過createStore方法創(chuàng)建的store是一個對象肃续,它本身又包含4個方法
- getState():獲取store中當(dāng)前的狀態(tài)
- dispatch(action): 分發(fā)一個action黍檩,并返回這個action,這是唯一能改變 store 中數(shù)據(jù)的方式
- subscribe(listener):注冊一個監(jiān)聽者痹升,它在store發(fā)生變化時被調(diào)用
- replaceReducer(nextReducer):更新當(dāng)前store里的reducer建炫,一般只會在開發(fā)模式中調(diào)用該方法
與React綁定
react-redux提供了一個組件和一個API幫助Redux和React進(jìn)行綁定,一個是React組件<Provider />,一個是 connect(). <Provider />接受一個store作為props疼蛾,是整個Redux應(yīng)用的頂層組件肛跌,connect()提供整個React應(yīng)用的任意組件中獲取store中數(shù)據(jù)的功能。
Redux middleware
它提供了一個分類處理action的機(jī)會察郁。
面對多樣的業(yè)務(wù)場景衍慎,單純地修改 dispatch 或 reducer 的代碼顯然不具有普適性,我們需要的是可以組合的皮钠、自由插拔的插件機(jī)制稳捆,這一點(diǎn) Redux借鑒了 Koa (它是用于構(gòu)建 Web 應(yīng)用的Node.js 框架)里 middleware 的思想。
理解middleware機(jī)制
Redux提供了applyMiddleware方法來加載middleware麦轰。
Redux中的applyMiddleware源碼
import compose from './compose'
export default function applyMiddleware(...middlewares) {
return (next) => (reducer, initialState) => {
let store = next(reducer, initialState)
let dispatch = store.dispatch
let chain = []
// 把store的getState方法和dispatch方法分別直接或間接賦值給middlewareAPI
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
// 讓每個middleware帶著middlewareAPI這個參數(shù)分別執(zhí)行一遍
// 執(zhí)行完后乔夯,獲得數(shù)組 [f1,f2,f3,f4,...,fn]
// middlewareAPI第二個箭頭函數(shù)返回的匿名函數(shù),因?yàn)殚]包款侵,每個匿名函數(shù)都可以訪問相同的store末荐,即middlewareAPI
/*
middlewareAPI中的dispatch為什么要用匿名函數(shù)包裹呢?
我們用 applyMiddleware 是為了改造 dispatch,所以 applyMiddleware 執(zhí)行完后新锈,dispatch 是 變化了的甲脏,而 middlewareAPI 是 applyMiddleware 執(zhí)行中分發(fā)到各個 middleware 的,所以 必須用匿名函數(shù)包裹 dispatch妹笆,這樣只要 dispatch 更新了块请,middlewareAPI 中的 dispatch 應(yīng) 用也會發(fā)生變化。
*/
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
- 函數(shù)式編程思想設(shè)計
middleware設(shè)計的是一層層包裹的匿名函數(shù)拳缠,這其實(shí)是函數(shù)式編程中的currying墩新,他是一種私用匿名單參數(shù)函數(shù)來實(shí)現(xiàn)多參數(shù)函數(shù)的方法。
currying的middleware結(jié)構(gòu)的好處有以下兩點(diǎn):
- 易串聯(lián):currying函數(shù)具有延遲執(zhí)行的特性窟坐,通過不斷currying形成的middleware可以累積參數(shù)海渊,再配合組合(compose)的方式,很容易形成pipeline來處理數(shù)據(jù)流
- 共享store:在applyMiddleware執(zhí)行的過程中狸涌,store還是舊的切省,但是因?yàn)殚]包的存在最岗,applyMiddleware完成后帕胆,所有的middleware內(nèi)部拿到的store是最新且相同的。
import { createStore, applyMiddleware, compose } from 'Redux';
import rootReducer from '../reducers';
const finalCreateStore = compose(
// 在開發(fā)環(huán)境中使用 middleware
applyMiddleware(d1, d2, d3),
DevTools.instrument()
)
給middleware分發(fā)store
let newStore = applyMiddleware(mid1,mid2,mid3,...)(createStore)(reducer, null)
看注釋般渡。組合串聯(lián)middleware
dispatch = compose(...chain)(store.dispatch);
// compose 實(shí)現(xiàn)方式
function compose(...funs) {
return arg => func.reduceRight((composed, f) => f(composed), arg)
}
compose(...funcs)返回一個匿名函數(shù)懒豹,其中funcs就是chain數(shù)組芙盘。當(dāng)調(diào)用reduceRight時,依次從funcs數(shù)組的右端取一個函數(shù)fx拿來執(zhí)行脸秽,fx的參數(shù)composed就是前一次fx+1執(zhí)行的結(jié)果儒老,而第一次執(zhí)行的fn(n代表chain的長度)的參數(shù)arg就是 store.dispath.假設(shè)n=3
dispatch = f1(f2(f3(store.dispatch)))
這時調(diào)用新dispatch時,每一個middleware都依次執(zhí)行了记餐。
- 在middleware中調(diào)用dispatch會發(fā)聲什么
compose之后驮樊,所有的middleware算是串聯(lián)起來了∑停可是還有一個問題囚衔,在分發(fā)store時,我們提到過每個middleware都可以訪問store雕沿,即middlewareAPI這個變量练湿,也可以拿到store的dispatch屬性。那么审轮,在middleware中調(diào)用store.dispatch()會發(fā)生什么肥哎,和調(diào)用next()有什么區(qū)別?
const logger = store => next => action => {
console.log('dispatch:', action);
next(action);
console.log('finish:', action);
};
const logger = store => next => action => { c
onsole.log('dispatch:', action);
store.dispatch(action);
console.log('finish:', action);
};
在分發(fā) store 時我們解釋過疾渣,middleware 中 store 的 dispatch 通過匿名函數(shù)的方式和最終 compose 結(jié)束后的新 dispatch 保持一致篡诽,所以,在 middleware 中調(diào)用 store.dispatch() 和在其他 任何地方調(diào)用的效果一樣稳衬。而在 middleware 中調(diào)用 next()霞捡,效果是進(jìn)入下一個 middleware。
這就是一個洋蔥模型
next代表下一個執(zhí)行的中間件薄疚,每次return回去的都是一個未執(zhí)行的函數(shù)碧信,只有最后調(diào)用才能執(zhí)行。
// 實(shí)現(xiàn)之后的效果
// 這時候返回的是一個經(jīng)過層層中間件封裝的dispatch街夭,新的dispatch函數(shù)
newDispatch = M1(M2(M3(dispatch)))
// M1中的next 就是M2
// M2中的next就是M3
// M3中的next就是dispatch砰碴,執(zhí)行dispatch
// 調(diào)用
newDispatch(action)
正常情況下,如上圖左板丽,我們分發(fā)一個action時呈枉,middleware通過next(action)一層層傳遞和處理action直到Redux原生的dispathc。當(dāng)某個middleware使用store.dispatch(action)分發(fā)action埃碱,會發(fā)聲右圖的情況猖辫,就會形成無限循環(huán)。那么store.dispatch(action)的用武之地在哪里呢砚殿?
異步請求的時候啃憎,使用到Redux Thunk
const thunk = store => next => action => {
typeof action === 'function'?
action(store.dispatch, store.getState) : next(action)
}
Redux Thunk會判斷action是否是函數(shù)。如果是似炎,執(zhí)行action辛萍,否則繼續(xù)傳遞action到下一個middleware悯姊。
const getThenShow = (dispatch, getState) => {
const url = 'http://xxx.json'
fetch(url)
.then((res) => {
dispatch({
type: 'SHOW_MESSAGE_FOR_ME',
message: res.json(),
})
}).catch( ()=> {
dispatch({
type: 'FETCH_DATA_FAIL',
message: 'error'
})
} )
}
// 再應(yīng)用中調(diào)用 store.dispatch(getThenShow)
Redux異步流
使用middleware簡化異步請求
- redux-thunk
Thunk函數(shù)實(shí)現(xiàn)上就是針對多參數(shù)的currying以實(shí)現(xiàn)對函數(shù)的惰性求值。任何函數(shù)贩毕,只要參數(shù)有回調(diào)函數(shù)悯许,就能寫成Thunk函數(shù)的形式。
redux-thunk的源代碼:
function createThunkMiddleware(extraArg) {
return ({dispath, getState} => next => action => {
if ( typeof action === 'function' ) {
return action(dispatch, getState, extraArg)
}
return next(action)
})
}
- redux-promise
抽象promise來解決異步流問題辉阶。
redux-promise 兼容了 FSA 標(biāo)準(zhǔn)先壕,也就是說將返回的結(jié)果保存在 payload 中。實(shí)現(xiàn)過程非常容易理解谆甜,即判斷 action 或action.payload是否為 promise启上,如果是,就執(zhí)行 then店印,返回的結(jié)果再發(fā)送一次 dispatch冈在。
使用ES7的async和await語法,簡化異步過程
const fetchData = (url, params) => fetch(url, params);
async function getWeather(url, params) {
const result = await fetchData(url, params);
if( result.error ) {
return {
type: 'GET_WEATHER_ERROR',
error: result.error
}
}
return {
type: 'GET_WEATHER_SUCCESS',
payload: result
}
}
- redux-composable-fetch
實(shí)際請求中按摘,加上loading狀態(tài)
這時候異步請求的action
{
url: '/api/weather.json',
params: {
city: encodeURI(city),
},
types: ['GET_WEATHER', 'GET_WEATHER_SUCESS', 'GET_WEATHER_ERROR'],
}
和FSA不一樣了包券,沒有types,有了url和type代表請求狀態(tài)
const fetchMiddleware = store => next => action => {
if (!action.url || !Array.isArray(action.types)) {
return next(action);
}
const [LOADING, SUCCESS, ERROR] = action.types;
next({
type: LOADING,
loading: true,
...action,
});
fetch(action.url, { params: action.params })
.then(result => {
next({
type: SUCCESS,
loading: false,
payload: result,
});
})
.catch(err => {
next({
type: ERROR,
loading: false,
error: err,
});
});
}
使用middleware處理復(fù)雜異步流
1.輪詢
多異步串聯(lián)
使用Promiseredux-saga
最優(yōu)雅通用的解決方法炫贤,有靈活而強(qiáng)大的協(xié)程機(jī)制溅固,可以解決任何復(fù)雜的異步交互。
Redux和路由
我們需要一個這樣的路由系統(tǒng)兰珍,它既能利用React Router 的聲明式特性侍郭,又能將路由信息整合進(jìn) Redux store 中。
React Router
1.基本原理
-
React Router特性
React Router與React對比
- 聲明式的路由
// 實(shí)例
import { Router, Route, browserHistory } from 'react-router';
const routes = (
<Router history={browserHistory}>
<Route path='/' component{App} />
</Router>
)
- 嵌套路由及路徑匹配
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
const routes = (
<Router history={browserHistory}>
<Route path="/" component={App}>
<IndexRoute component={MailList} />
<Route path="/mail/:mailId" component={Mail} />
</Route>
</Router>
);
App 組件承載了顯示頂欄和側(cè)邊欄的功能掠河,而 React Router 會根據(jù)當(dāng)前的 url 自動判斷該顯示郵件列表頁還是詳情頁:
. 當(dāng) url 為 / 時亮元,顯示列表頁;
. 當(dāng) url 為 /mail/123 時唠摹,顯示詳情頁爆捞。
- 支持多種路由切換方式
hashChange 或是 history.pushState。hashChange 的方式擁有良好的瀏覽器兼容性勾拉,但是 url 中卻多了丑陋的 /#/ 部分煮甥;而 history.pushState 方法則能給我們提供優(yōu)雅的 url,卻需要額外的服務(wù)端配置解決任意路徑刷新的問題藕赞。
React Router Redux
當(dāng)我們采用 Redux 架構(gòu)時成肘,所有的應(yīng)用狀態(tài)必須放在一個單一的 store 中管理,路由狀態(tài)也不例外斧蜕。而這就是 React Router Redux 為我們實(shí)現(xiàn)的主要功能双霍。
- React Router與Redux store綁定
React Router Redux 提供了簡單直白的 API——syncHistoryWithStore 來完成與 Redux store的綁定工作。
import { browserHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
import reducers from '<project-path>/reducers'
const store = createStore(reducers);
const history = syncHistoryWithStore(browserHistory, store);
- 用Redux的方式改變路由
對Redux的store進(jìn)行增強(qiáng),以便分發(fā)的action能被正確識別
import { browserHistory } from 'react-router';
import { routerMiddleware } from 'react-router-redux';
const middleware = routerMiddleware(browserHistory);
const store = createStore(
reducers,
applyMiddleware(middleware)
);
使用
import { push } from 'react-router-redux';
// 切換路由到 /home
store.dispatch(push('/home'));
Redux 與 組件
Redux中店煞,強(qiáng)調(diào)了3中不同類型的布局組件:Layouts、Views和Components风钻。它常常是無狀態(tài)函數(shù)顷蟀,傳入主體內(nèi)容的children屬性。
const Layout = ({ children } => {
<div className = 'container'>
<Header />
<div className="contaier">
{ children }
</div>
</div>
})
- Views
子路由入口組件骡技,描述子路由入口的基本結(jié)構(gòu)鸣个,包含此路由下所有的展示型組件。
@connect((state) => {
//...
})
class HomeView extends Component {
render() {
const { sth, changeType } = this.props;
const cardProps = { sth, changeType };
return (
<div className="page page-home">
<Card {...cardProps} />
</div>
);
}
}
3布朦、Components
末級渲染組件囤萤,描述了從路由以下的子組件。包含具體的業(yè)務(wù)邏輯和交互是趴,但所有的數(shù)據(jù)和action都是油Views傳下來涛舍。
class Card extends Components {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(opts) {
const { type } = opts;
this.props.changeType(type);
}
render() {
const { sth } = this.props;
return (
<div className="mod-card">
<Switch onChange={this.handleChange}>
// ...
</Switch>
{sth}
</div>
);
}
}