Redux

Redux的核心運(yùn)作流程

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 三大原則

  1. 單一數(shù)據(jù)源
    Redux思想中,一個應(yīng)用永遠(yuǎn)只有唯一的數(shù)據(jù)源刃榨。combineReducers化解了數(shù)據(jù)源對象過于龐大的問題弹砚。

  2. 狀態(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)的目的。

  3. 狀態(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ī)會察郁。


Redux同步數(shù)據(jù)流動

面對多樣的業(yè)務(wù)場景衍慎,單純地修改 dispatch 或 reducer 的代碼顯然不具有普適性,我們需要的是可以組合的皮钠、自由插拔的插件機(jī)制稳捆,這一點(diǎn) Redux借鑒了 Koa (它是用于構(gòu)建 Web 應(yīng)用的Node.js 框架)里 middleware 的思想。


應(yīng)用middleware后Redux處理事件的邏輯

理解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
    }
  }
}
  1. 函數(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()
)
  1. 給middleware分發(fā)store
    let newStore = applyMiddleware(mid1,mid2,mid3,...)(createStore)(reducer, null)
    看注釋般渡。

  2. 組合串聯(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í)行了记餐。

  1. 在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)
Redux middleware流程圖

正常情況下,如上圖左板丽,我們分發(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簡化異步請求

  1. 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)
  })
}
  1. 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
  }
}
  1. 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.輪詢

  1. 多異步串聯(lián)
    使用Promise

  2. redux-saga
    最優(yōu)雅通用的解決方法炫贤,有靈活而強(qiáng)大的協(xié)程機(jī)制溅固,可以解決任何復(fù)雜的異步交互。

Redux和路由

我們需要一個這樣的路由系統(tǒng)兰珍,它既能利用React Router 的聲明式特性侍郭,又能將路由信息整合進(jìn) Redux store 中。

React Router

1.基本原理


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)的主要功能双霍。

  1. 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);
  1. 用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>
})
  1. 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>
);
}
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市唆途,隨后出現(xiàn)的幾起案子富雅,更是在濱河造成了極大的恐慌,老刑警劉巖肛搬,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異温赔,居然都是意外死亡蛤奢,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門陶贼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來啤贩,“玉大人,你說我怎么就攤上這事拜秧」衔睿” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵腹纳,是天一觀的道長痢掠。 經(jīng)常有香客問我,道長嘲恍,這世上最難降的妖魔是什么足画? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮佃牛,結(jié)果婚禮上淹辞,老公的妹妹穿的比我還像新娘。我一直安慰自己俘侠,他們只是感情好象缀,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布蔬将。 她就那樣靜靜地躺著,像睡著了一般央星。 火紅的嫁衣襯著肌膚如雪霞怀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天莉给,我揣著相機(jī)與錄音毙石,去河邊找鬼。 笑死颓遏,一個胖子當(dāng)著我的面吹牛徐矩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播叁幢,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼滤灯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了曼玩?” 一聲冷哼從身側(cè)響起力喷,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎演训,沒想到半個月后弟孟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡样悟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年拂募,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窟她。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡陈症,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出震糖,到底是詐尸還是另有隱情录肯,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布吊说,位于F島的核電站洞豁,受9級特大地震影響篡九,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一孝情、第九天 我趴在偏房一處隱蔽的房頂上張望惕蹄。 院中可真熱鬧唆迁,春花似錦逢享、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽懈凹。三九已至,卻和暖如春悄谐,著一層夾襖步出監(jiān)牢的瞬間介评,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工尊沸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贤惯。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓洼专,卻偏偏與公主長得像,于是被迫代替她去往敵國和親孵构。 傳聞我的和親對象是個殘疾皇子屁商,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 做React需要會什么蜡镶? react的功能其實(shí)很單一,主要負(fù)責(zé)渲染的功能恤筛,現(xiàn)有的框架官还,比如angular是一個大而...
    蒼都閱讀 14,760評論 1 139
  • 一、什么情況需要redux毒坛? 1望伦、用戶的使用方式復(fù)雜 2、不同身份的用戶有不同的使用方式(比如普通用戶和管...
    初晨的筆記閱讀 2,030評論 0 11
  • 學(xué)習(xí)必備要點(diǎn): 首先弄明白煎殷,Redux在使用React開發(fā)應(yīng)用時屯伞,起到什么作用——狀態(tài)集中管理 弄清楚Redux是...
    賀賀v5閱讀 8,904評論 10 58
  • http://gaearon.github.io/redux/index.html ,文檔在 http://rac...
    jacobbubu閱讀 79,967評論 35 198
  • 前言 本文 有配套視頻豪直,可以酌情觀看劣摇。 文中內(nèi)容因各人理解不同,可能會有所偏差弓乙,歡迎朋友們聯(lián)系我討論末融。 文中所有內(nèi)...
    珍此良辰閱讀 11,906評論 23 111