Redux

Redux的核心運作流程

Redux這個npm包,提供若干API讓我們使用reducer創建store,并能更新store中的數據或獲取store中最新的狀態。“Redux應用”指使用redux結合視圖層實現(React)及其他前端應用必備組件(路由庫、Ajax請求庫)組成的完成類Flux思想的前端應用。

Redux 三大原則

  1. 單一數據源
    Redux思想中,一個應用永遠只有唯一的數據源。combineReducers化解了數據源對象過于龐大的問題。

  2. 狀態是只讀的
    Redux中,我們并不會自己用代碼來定義一個store。取而代之的是,我們定義一個rducer,它的功能是根據當前觸發的action對當前應用的狀態(state)進行迭代,這里我們斌沒有直接修改應用的狀態,而是返回一份全新的狀態。
    Redux提供的createStore方法會根據reducer生成store。最后,我們利用 store.dispatch 方法來達到修改狀態的目的。

  3. 狀態修改均有純函數完成
    在Redux中,我們通過定義reducer來確定狀態的修改。

Redux核心API

Redux的核心是一個store,這個store由Redux提供的 createStore(reducers, [,initialState])方法生成。

Redux里,負責響應action并修改數據的角色是reducer。其函數簽名為reducer(previousState, action ) => newState。所以,reducer的職責就是根據 previousState 和 action 計算出新的 newState。

// MyReducer.js
const initialState = {
  todos: [],
}

// 我們定義的todos這個reducer在第一次執行的時候,會返回  { todos: [] }作為初始化狀態
function todos(previousState = initalState, action) {
  switch(action.type) {
    case 'xx': {
      // 具體的業務邏輯
    }

    default: 
      return previousState;
  }
}

Redux = Reducer + Flux
通過createStore方法創建的store是一個對象,它本身又包含4個方法

  • getState():獲取store中當前的狀態
  • dispatch(action): 分發一個action,并返回這個action,這是唯一能改變 store 中數據的方式
  • subscribe(listener):注冊一個監聽者,它在store發生變化時被調用
  • replaceReducer(nextReducer):更新當前store里的reducer,一般只會在開發模式中調用該方法

與React綁定

react-redux提供了一個組件和一個API幫助Redux和React進行綁定,一個是React組件<Provider />,一個是 connect(). <Provider />接受一個store作為props,是整個Redux應用的頂層組件,connect()提供整個React應用的任意組件中獲取store中數據的功能。

Redux middleware

它提供了一個分類處理action的機會。


Redux同步數據流動

面對多樣的業務場景,單純地修改 dispatch 或 reducer 的代碼顯然不具有普適性,我們需要的是可以組合的、自由插拔的插件機制,這一點 Redux借鑒了 Koa (它是用于構建 Web 應用的Node.js 框架)里 middleware 的思想。


應用middleware后Redux處理事件的邏輯

理解middleware機制

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這個參數分別執行一遍
    // 執行完后,獲得數組  [f1,f2,f3,f4,...,fn]
    // middlewareAPI第二個箭頭函數返回的匿名函數,因為閉包,每個匿名函數都可以訪問相同的store,即middlewareAPI

  /*
middlewareAPI中的dispatch為什么要用匿名函數包裹呢?
我們用 applyMiddleware 是為了改造 dispatch,所以 applyMiddleware 執行完后,dispatch 是 變化了的,而 middlewareAPI 是 applyMiddleware 執行中分發到各個 middleware 的,所以 必須用匿名函數包裹 dispatch,這樣只要 dispatch 更新了,middlewareAPI 中的 dispatch 應 用也會發生變化。
*/
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
  1. 函數式編程思想設計
    middleware設計的是一層層包裹的匿名函數,這其實是函數式編程中的currying,他是一種私用匿名單參數函數來實現多參數函數的方法。
    currying的middleware結構的好處有以下兩點:
  • 易串聯:currying函數具有延遲執行的特性,通過不斷currying形成的middleware可以累積參數,再配合組合(compose)的方式,很容易形成pipeline來處理數據流
  • 共享store:在applyMiddleware執行的過程中,store還是舊的,但是因為閉包的存在,applyMiddleware完成后,所有的middleware內部拿到的store是最新且相同的。
import { createStore, applyMiddleware, compose } from 'Redux';
import rootReducer from '../reducers';

const finalCreateStore = compose(
    // 在開發環境中使用 middleware
    applyMiddleware(d1,  d2, d3),
    DevTools.instrument()
)
  1. 給middleware分發store
    let newStore = applyMiddleware(mid1,mid2,mid3,...)(createStore)(reducer, null)
    看注釋。

  2. 組合串聯middleware
    dispatch = compose(...chain)(store.dispatch);

// compose 實現方式
function compose(...funs) {
  return arg => func.reduceRight((composed, f) => f(composed), arg)
}

compose(...funcs)返回一個匿名函數,其中funcs就是chain數組。當調用reduceRight時,依次從funcs數組的右端取一個函數fx拿來執行,fx的參數composed就是前一次fx+1執行的結果,而第一次執行的fn(n代表chain的長度)的參數arg就是 store.dispath.假設n=3
dispatch = f1(f2(f3(store.dispatch)))
這時調用新dispatch時,每一個middleware都依次執行了。

  1. 在middleware中調用dispatch會發聲什么
    compose之后,所有的middleware算是串聯起來了。可是還有一個問題,在分發store時,我們提到過每個middleware都可以訪問store,即middlewareAPI這個變量,也可以拿到store的dispatch屬性。那么,在middleware中調用store.dispatch()會發生什么,和調用next()有什么區別?
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);
};

在分發 store 時我們解釋過,middleware 中 store 的 dispatch 通過匿名函數的方式和最終 compose 結束后的新 dispatch 保持一致,所以,在 middleware 中調用 store.dispatch() 和在其他 任何地方調用的效果一樣。而在 middleware 中調用 next(),效果是進入下一個 middleware。

這就是一個洋蔥模型
next代表下一個執行的中間件,每次return回去的都是一個未執行的函數,只有最后調用才能執行。

// 實現之后的效果
// 這時候返回的是一個經過層層中間件封裝的dispatch,新的dispatch函數
newDispatch = M1(M2(M3(dispatch)))
// M1中的next 就是M2
// M2中的next就是M3
// M3中的next就是dispatch,執行dispatch
// 調用
newDispatch(action)
Redux middleware流程圖

正常情況下,如上圖左,我們分發一個action時,middleware通過next(action)一層層傳遞和處理action直到Redux原生的dispathc。當某個middleware使用store.dispatch(action)分發action,會發聲右圖的情況,就會形成無限循環。那么store.dispatch(action)的用武之地在哪里呢?
異步請求的時候,使用到Redux Thunk

const thunk = store => next => action => {
  typeof action === 'function'?
    action(store.dispatch, store.getState) : next(action)
}

Redux Thunk會判斷action是否是函數。如果是,執行action,否則繼續傳遞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'
      })
    } )
}

// 再應用中調用 store.dispatch(getThenShow)

Redux異步流

使用middleware簡化異步請求

  1. redux-thunk
    Thunk函數實現上就是針對多參數的currying以實現對函數的惰性求值。任何函數,只要參數有回調函數,就能寫成Thunk函數的形式。
    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 標準,也就是說將返回的結果保存在 payload 中。實現過程非常容易理解,即判斷 action 或action.payload是否為 promise,如果是,就執行 then,返回的結果再發送一次 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
    實際請求中,加上loading狀態
    這時候異步請求的action
{
url: '/api/weather.json',
params: {
city: encodeURI(city),
},
types: ['GET_WEATHER', 'GET_WEATHER_SUCESS', 'GET_WEATHER_ERROR'],
}

和FSA不一樣了,沒有types,有了url和type代表請求狀態

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處理復雜異步流

1.輪詢

  1. 多異步串聯
    使用Promise

  2. redux-saga
    最優雅通用的解決方法,有靈活而強大的協程機制,可以解決任何復雜的異步交互。

Redux和路由

我們需要一個這樣的路由系統,它既能利用React Router 的聲明式特性,又能將路由信息整合進 Redux store 中。

React Router

1.基本原理


React Router流程圖
  1. React Router特性


    React Router與React對比
  • 聲明式的路由
// 實例
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 組件承載了顯示頂欄和側邊欄的功能,而 React Router 會根據當前的 url 自動判斷該顯示郵件列表頁還是詳情頁:
. 當 url 為 / 時,顯示列表頁;
. 當 url 為 /mail/123 時,顯示詳情頁。

  • 支持多種路由切換方式
    hashChange 或是 history.pushState。hashChange 的方式擁有良好的瀏覽器兼容性,但是 url 中卻多了丑陋的 /#/ 部分;而 history.pushState 方法則能給我們提供優雅的 url,卻需要額外的服務端配置解決任意路徑刷新的問題。

React Router Redux

當我們采用 Redux 架構時,所有的應用狀態必須放在一個單一的 store 中管理,路由狀態也不例外。而這就是 React Router Redux 為我們實現的主要功能。

  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進行增強,以便分發的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中,強調了3中不同類型的布局組件:Layouts、Views和Components。它常常是無狀態函數,傳入主體內容的children屬性。

const Layout = ({ children } => {
  <div className = 'container'>
    <Header />
    <div className="contaier">
      { children }
    </div>
  </div>
})
  1. Views
    子路由入口組件,描述子路由入口的基本結構,包含此路由下所有的展示型組件。
@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
末級渲染組件,描述了從路由以下的子組件。包含具體的業務邏輯和交互,但所有的數據和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>
);
}
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容