探究redux源碼-衍生-中間件思想

本文主要是閱讀redux實現方式的時候,思路的一些拓展。大概是在三四個月前就看過redux源碼,一直想寫一些東西。可是迫於項目的緊急性,以及我的能力精力有限,就擱淺了。如今又從新看,並且不少時候,看懂一些東西可能不難,可是真正深刻進去研究,會發現不少東西並非很清楚,這就須要多思考一些,再寫下來能有清晰的思路就更難了。此次的文章須要你對redux,react-redux都有必定的瞭解,不少地方我沒有作過多的解釋,還有本文不完美的地方,還請指出。javascript

redux基礎

  • 咱們先大概過一下redux暴露的幾個方法。
// index.js
export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
}複製代碼
  • createStore
    一個工廠函數傳入reducer,建立store,返回幾個函數,主要是dispatch,getState,subscribe,replaceReducer,以及結合rx這種發佈訂閱庫的symbol($$observable)html

  • combineReducers
    把單個的reducer組合成一個大的reducerjava

  • bindActionCreators
    把咱們寫的一個js中的好多ActionCreator 經過遍歷搞的一個對象裏,並返回。node

  • applyMiddleware
    一個三階函數,是用來改寫store的dispatch方法,並把全部的中間件都compose串聯起來,經過改寫dispatch,來實現redux功能的拓展。react

  • compose
    一個組合多個middleware的方法,經過reduceRight方法(同理也能夠是reduce),把傳進來的middleware串成一條鏈,也能夠當作回調接回調,一個預處理的方法。git

redux-middleware

接觸事後端的同窗,對中間件這個概念必定不陌生。像node中的express,koa框架,middleware都起到了重要做用。redux中的實現方式不太同樣,不過原理思想都是差很少的,都是鏈式組合,能夠應用多箇中間件。它提供了action發起以後,到達reducer以前的拓展功能。能夠利用Redux middleware 來進行日誌記錄、建立崩潰報告、調用異步接口或者路由等等。github

咱們從redux中applyMiddleware使用入口開始研究。web

中間件express

//日誌中間件1
  const logger1 = store => next => action => {
    console.log('logger1 start', action);
    next(action);
    console.log('logger1 end', action);
  }
  
  //日誌中間件2
  const logger2 = store => next => action => {
    console.log('logger2 start', action);
    next(action);
    console.log('logger2 end', action);
  }複製代碼

爲何中間件要定義成這種三階的樣子呢,固然是中間件的消費者(applyMiddleware)規定的。編程

先經過一個小栗子看一下middleware的使用。

//定義一個reducer
  const todoList = [];
  function addTodo(state = todoList, action) {
    switch (action.type) {
      case 'ADD_TODO':
        return [...state, action.text];
        break;
      default:
        return state;
    }
  }

 //建立store
 //爲了先減輕其餘方法帶來的閱讀困難,我選用直接使用applyMiddleware的方法建立store
  
  import { createStore, applyMiddleware } from 'redux';
  
  const store = applyMiddleware(logger1, logger2)(createStore)(reducer);
  

 // store注入Provider    
  ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));

複製代碼

經過applyMiddleware執行能夠獲得一個store,store再經過react-redux中的provider注入。此時獲得的store就是被改造了dispatch的。經過圖來形象的解釋一下:

  • 默認的redux流程

  • applyMiddleware封裝以後

能夠看出redux在事件或者某個函數調用後,執行action(多是bindActionCreators處理後的),因爲bindActionCreator會去調用dispatch,

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}複製代碼

dispatch內部會把currenReducer執行,並把監聽者執行。實現view更新。
可是通過applyMiddleware的包裝,store裏面的被封裝,在調動action以後,執行封裝後的dispatch就會通過一系列的中間件處理,再去觸發reducer。

而後咱們再經過研究源碼,看他是怎麼實現的封裝dispatch。

思路能夠從經過applyMiddleware建立store一點一點的看。

//applyMiddleware 源碼

middlewares => createStore => (reducer, preloadedState) => {

 // 第一步先建立一個store
  var store = createStore(reducer, preloadedState, enhancer)
  
 // 緩存dispatch,原store的dispatch要改寫。
  var dispatch = store.dispatch
  
 // 定義chain來存放 執行後的二階中間件
  var chain = []

 // middleware 柯理化的第一個參數。參照logger1的store,這裏只保留getState,和改造後的dispatch兩個方法。
  var middlewareAPI = {
    getState: store.getState,
    dispatch: (action) => dispatch(action)
  }
  
  // 把中間件處理一層,把getState,dispatch方法傳進去,也就是中間件柯理化第一次的store參數。
  // 這樣能保證每一箇中間件的store都是同一個,柯理化的用途就是預置參數嘛。
  chain = middlewares.map(middleware => middleware(middlewareAPI))
  
  // 串聯起全部的中間件,dispatch從新賦值,這樣調用dispatch的時候,就會穿過全部的中間件。
  dispatch = compose(...chain)(store.dispatch)

  return {
    ...store,
    dispatch
  }
}

複製代碼

compose仍是比較重要的

//compose
其實compose是函數式編程中比較重要的一個方法。上面調用compose的時候可見是一個二階函數。
 
const compose = (...funcs) => {
  
  //沒有參數,那就返回一個function
  if (!funcs.length) {
    return arg => arg
  }
  //一箇中間件,返回它
  if (funcs.length === 1) {
    return funcs[0];
  }
  // 最後一個
  var last = funcs[funcs.length -1];
  
  // 複製一份,除去last
  var rest = funcs.slice(0, -1);
  
  // 返回函數,能夠接收store.dispatch。
  // reduceRight 反向迭代。
  
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}

複製代碼

compose執行

  • chain中都是已經預置middlewareAPI參數後的二階函數。執行傳入的參數都是 形參next。

  • 經過執行compose(...chain)(store.dispatch),last是最後一箇中間件,執行並傳入 store.dispatch, 返回一個只剩一階的(action) => {}, 不過已經預置了next參數,也就是store.dispatch

  • 而後last(...args)返回的結果傳入reduceRight的回調, 對應形參是composed。

  • f是rest的最後一項, 執行並把 composed 傳入,等同於f形參中的next... 獲得的結果也是一階函數,預置的next是last(...args) ...

  • 以此類推。這樣,就造成了一個嵌套多層的語句。
    相似於logger1(logger2(store.dispatch),固然這只是一個比喻。
    只不過到第一個middleware的時候,是二階函數傳入next執行,獲得一階函數返回賦值給dispatch,這時的一階函數已經變成了形似這樣:

    function (action) {
      console.log('logger1 start', action);
      next(action);
      console.log('logger1 end', action);
    }
    複製代碼

通過compose以後的dispatch執行

  • 返回的store中dispatch被修改,執行store.dispatch的時候,也就是這個函數執行.

  • 當執行到next(action)的時候,會調用已經預置的next函數,也就是第二個中間件的(action) => {},依次類推。直到最後一箇中間件,他的next函數是store.dispatch函數,執行並把action傳入。

  • 執行完最後一箇中間件的next(action),也就是初始的dispatch。next後面的代碼再執行,再逆向把中間件走一遍,直到第一個中間件執行完畢。
    就會出現這種效果

    start logger1 Object {type: "ADD_TODO", text: "defaultText"}
    start logger2 Object {type: "ADD_TODO", text: "defaultText"}
    dispatch()
    end logger2 Object {type: "ADD_TODO", text: "defaultText"}
    end logger1 Object {type: "ADD_TODO", text: "defaultText"}
    複製代碼

用圖形象點就是

這樣redux middleware的執行流程就搞清楚了。

應用applyMiddleware的方式

import { createStore, applyMiddleware } from 'redux';

1. compose(applyMiddleware(logger1, logger2))(createStore)(reducer);

2. applyMiddleware(logger1, logger2)createStore)(reducer);

3. createStore(reducer, [], applyMiddleware(logger1, logger2));複製代碼

createStore源碼中有一個判斷,

createStore(reducer, preloadedState, enhancer) => {
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    // 因此第三種直接傳入applyMiddleware(logger1, logger2),效果是同樣的。
    return enhancer(createStore)(reducer, preloadedState)
  }
}複製代碼

第一種先compose同理。一個參數的時候會返回applyMiddleware,變形以後也是同樣的。

enhancer的用法不少種,不只僅是applyMiddleware,好比Redux-Devtools, 都是利用了compose函數。自定義開發一些拓展功能仍是很強大的...
redux裏的compose是處理三階函數的,恰巧createStore, applyMiddleware都是三階函數,均可以經過compose串聯起來。不由感嘆函數式編程思惟的強大啊。

應用異步action

  • redux-thunk

簡單來講,就是dispatch(action), action 能夠是function. 固然這種寫法須要配合bindActionCreator處理。

actionCreator以前都是返回一個{type: 'UPDATE', text: 'aaa'}這樣的簡單對象。經過thunk中間件,能夠處理返回function的狀況。

const reduxThunk = store => next => action => {
  if (typeof action === 'function') {
    console.log('thunk');
    return action(store.dispatch);
  }
  return next(action);
}

//action 多是這樣。
const addAsync = function() {
  return (dispatch) => {
    setTimeout(() => {
      dispatch({ type: 'ADD_TODO', text: 'AsyncText' })
    }, 1000)
  }
}

複製代碼
  • redux-promise

用來處理actions返回的是promise對象的狀況。其實道理很簡單,thunk去判斷傳進中間件的action是否是function,這裏就判斷是否是promise就好了。

//判斷promise
function isPromise(val) {
  return val && typeof val.then === 'function';
}

const reduxPromise = store => next => action => {
  return isPromise(action)
    ? action.then(store.dispatch)
    : next(action);
}

// 源碼還多了一個判斷,判斷action是否是標準的flux action對象(簡單對象,包含type屬性...)複製代碼

express中的middleware

當一個客戶端的http請求過來的時候,匹配路由處理先後,會通過中間件的處理,好比一些CORS處理,session處理...

  • 用法

    var app = express();
    
    app.use(function (req, res, next) {
      console.log('Time:', Date.now());
      next();
    });
    
    app.use(middleware1);
    app.use(middleware2);
    app.use(middleware3);
    
    app.listen(3000);複製代碼

    每次訪問這個app應用的時候,都會執行

  • 模擬

看了源碼,本身模擬一下,固然是很簡單的用法了。這是應用層的中間件,要實現路由器層的話,只須要根據路由 保存不一樣的數組就行了,而後匹配。

const http = require('http');
function express () {
  const app = function(req, res) {
    let index = 0;
    //重點在於next函數的實現,express是用一個數組維護的。
    function next() {
      const routes = app.route;
      routes[index++](req, res, next);
    }
    next();
  };
  
  app.route = [];
  
  // 很明顯use 是往數組裏push。
  app.use = function (callback) {
    this.route.push(callback);
  };
  
  // listen函數是一個語法糖,利用http模塊
  app.listen = function(...args) {
    http.createServer(app).listen(...args);
  }

  return app;
}

const app = express();

app.use((req, res, next) => {
  setTimeout(() => {
    console.log('async');
    next();
  }, 1000);
});

app.use((req, res, next) => {
  console.log( 'logger request url:', req.url);
  next();
});


app.listen(3333);
複製代碼

假總結

如今web的中間件概念,都區別於最先嚴格意義上的中間件,其實咱們如今的不少編程思想都是借鑑的先驅提出的一些東西。JAVA中相似的是AOP,即面向切面編程,以補充OOP(面向對象)多個對象公用某些方法時形成的耦合。

目前js中見到的中間件思想用法都是差很少的,只有調用next,程序纔會繼續往下執行,沒有next,能夠拋出異常等。只不過redux使用的函數式編程思想,用法偏函數式一些。

demo代碼我會放到middleware-demo目錄裏,能夠clone下來操做一番。連接

先到這,下次衍生就是函數式編程了。







redux入門之三大原則

三個概念

  • Store

  • Action

  • Reducer

三大準則

  • 單一數據源

    整個應用狀態,都應該被存儲在單一store的對象樹中(object tree)。

  • State 是隻讀的

    惟一改變state的方法,就是發送(dispatch)一個動做(Action),action 是一個用於描述已發生事件的普通對象

  • 使用純函數去修改狀態

    爲了描述action 如何改變 state tree,須要寫reducers

    reducer 只是一些純函數。。(pure function)可被當作是一個狀態機,在任什麼時候候,只要有相同的輸入,就會獲得相同的輸出

    function add1(a,b) {
        return a + b
    }
    var a = 0;
    function add2(b) {
        return a = a + b
    }
    add1(1,2)   add1(1,2) 
    
    add2(1)     add2(1)    
    複製代碼

    This is an source code.



reducer

  • 爲何叫reducer
    大概是因爲reducer函數都能做爲數組的reduce方法的參數,因此叫reducer的吧。
  • Array中的reduce
    reduce須要兩個參數,一個是回調函數,一個是初始值,沒有初始值,會默認把數組第一個當初始值,並從第二個開始

模擬數組的reduce方法

Array.prototype.reduce = function reduce (callback, init) {
  var i = 0;
  if(typeof init === 'undefined') {
    init = this[0];
    i = 1;
  }
  if(typeof callback !== 'function') {
    throw new Error(callback + ' is not function')
  }
  for( ;i< this.length; i++ ) {
    init = callback(init, this[i])
  }
  return init ;
}複製代碼

reduce的使用

var ary = [1,2,3];
console.log(ary.reduce((initialValue, next) => {
  console.log(initialValue, next);
  return next;
},0))
// 01  12  23  3複製代碼

寫一個簡單的reducer

function reducer (initialValue, next) {
  console.log(initialValue, next)
  switch (next) {
    case 1:
      return next;
      break;
    default:
      return initialValue
  }
}
// 這個reducer 判斷傳入的值next。是1 的話 返回結果是 next 也就是1 ,因此最後結果都是1
console.log(ary.reduce(reducer))
// 12  13  1複製代碼

reducer在redux中的做用

reducer的做用就是設計state結構,它能夠給定state 的初始值,更重要的是告訴store,根據對應的action如何更新state。 一般咱們的store須要多個reducer組合,成爲咱們最後的state tree

注意點

  • 保持reducer 的純淨

一般咱們的reducer是純函數(pure function) 即固定的輸入返回固定的輸出,沒有反作用,沒有API請求... 等等,以後咱們說爲何這麼作。
一般咱們在處理業務中,好比請求一個列表的數據並渲染。
舉個栗子

const initialState = {
  code: -1,
  data: [],
  isFetching: false
};
//初始化咱們的state,也就是沒有請求以前,咱們根據接口的數據格式作一個模擬

function List(state = initialState, action) {
  switch (action.type) {
// 這裏的types 一般是咱們保存這種常量的一個對象

    case types.FETCH_LIST_SUCCESS:
      return {...state, data:action.data,isFetching:false};
    case types.FETCHING_LIST:
      return {...state, isFetching: true}
    case types.FETCH_LIST_FAILURE:
      return {...state, isFetching:false};
    default:
      return state
    }
}
複製代碼

{...X,...X} 是淺複製,和Object.assign 相似都是淺複製,實現代碼:

<script type="text/javascript">
			var _extends = Object.assign || function(target) {
				for(var i = 1; i < arguments.length; i++) {
					var source = arguments[i];
					for(var key in source) {
						if(Object.prototype.hasOwnProperty.call(source, key)) {
							target[key] = source[key];
						}
					}
				}
				return target;
			};

			var a = {
				"a": {
					"b": {
						"c": 100,
						"d": 200,
						"e": {
							"f": 300
						}
					}
				}
			};
			var b = {
				"a": {
					"b": {
						"g": 400
					}
				}
			};

			console.log(_extends({}, a, b));
		</script>
複製代碼


咱們的reducer函數就是根據請求的狀態返回不一樣的數據,可是數據格式是必定的。Fetching就是 請求過程當中,好比咱們作一個loading效果可能須要這個。而後type是success就是成功咱們返回數據。這些請求都放到actions 中了,actions去處理邏輯,數據API,重組數據。只須要傳給reducer函數數據結果就ok了。

爲何要從新返回一個對象。

咱們能夠看到reducer函數在拿到數據後經過Object.assign 從新返回一個對象,直接state.data 修改,返回state不行嗎?

首先 咱們默認的初始state是不能直接改變的,咱們的reducer函數 在數據failure的時候 return了默認的state,這個initialState 是不該該被修改的。

另外,咱們的react組件 會屢次接受store傳入props,每一次都應該是一個全新的對象引用,而不是同一個引用。好比咱們須要比較兩次傳入的props,利用componentWillReciveProps(nextProps) 比較this.props 跟nextProps,確定是須要兩個對象空間的,否則是同一個對象引用也就無法比較了。

因此redux 中的reducer 函數要求咱們必須返回新的對象state

redux文檔-reducer

多個reducer組合成咱們的state tree

一般咱們會引入redux提供的一個函數

import { combineReducers } from 'redux'
複製代碼

其實combineReducers作的事情很簡單,顧名思義就是合併多個reducer

好比咱們一個項目有多個reducer可是最後須要合併成一個,而後告訴store生成state tree,再注入Provider組件,先不關注Provider的問題。咱們看一下combineReducers的簡單實現

//首先咱們組合獲得的reducer仍舊是一個函數
//這個reducer會整合全部的reducer
//而後根據咱們定義的狀態樹的格式返回一個大的state tree
// 根據reducers這個對象的key,取到reducer函數,並傳入對應的 state

const combineReducers = function combineReducers (reducers) {
  return (state = {}, action) {
    Object.keys(reducers).reduce((initialState, key) => {
      initialState[key] = reducers[key](state[key], action)
      return initialState
    },{})

  }
}複製代碼

這個函數返回一個rootReducer,而後createStore接收rootReducer,在createStore內部會調用一次dispatch(init),rootReducer 會執行,全部咱們制定的reducrs對象中的key 都會被添加到 一個初始化initialState中,遍歷將每一個子級state添加到initialState 。init的時候,state[key]是undefined,每一個reducer函數有初始值 返回。之後的dispatch ,由於有了state tree,state[key]均可以取到值了。




store

store 是什麼

store是一個管理state的大對象,而且提供了一系列的方法

getState(),  //返回state
dispatch(action),  // 派發一個action
subscribe()  //訂閱監聽
複製代碼

經過redux 提供的 createStore,傳入reducer函數,咱們能夠獲得一個store對象

import { createStore } from 'redux'
const store = createStore(reducer)複製代碼

簡單實現一個createstore函數

//這是一個工廠函數,能夠建立store

const  createStore = (reducer) => {
   let state; // 定義存儲的state
   let listeners = [];
   
  //  getState的做用很簡單就是返回當前是state
  const  getState = ()=> state;
  
    //定義一個派發函數
    //當在外界調用此函數的時候,會修改狀態
  const dispatch = (action)=>{
      //調用reducer函數修改狀態,返回一新的狀態並賦值給這個局部狀態變量
      state = reducer(state,action);
      //依次調用監聽函數,通知全部的監聽函數
      listeners.forEach(listener => listener());
  }
   //訂閱此狀態的函數,當狀態發生變化的時候記得調用此監聽函數
  const subscribe = function(listener){
      //先把此監聽 加到數組中
      listeners.push(listener);
      
      //返回一個函數,當調用它的時候將此監聽函數從監聽數組移除
      return function(){
          listeners = listeners.filter(l => l != listener);
      }
  }
    //默認調用一次dispatch給state賦一個初始值
   dispatch({types: 'INIT'});
  return {
      getState,
      dispatch,
      subscribe
  }
}複製代碼

使用

function numReducer (state = 0, action) {
  switch (action.types) {
    case 'increase':
      return state + 1
      break;
    case 'deincrease':
      return state - 1
    default:
      return state
  }
}
let store = createStore(numReducer);
store.subscribe(() => {
  console.log('觸發了')
})
store.dispatch({types: 'increase'})
console.log(store.getState())
// 觸發了 1複製代碼

注意的點

根據官方的說法,一個應用應該只有一個store,即單一數據源,咱們經過合併reducer 來壯大state tree
未完





action

  • action至關於一個載體,它攜帶數據(多是用戶操做產生,或者接口數據),而且會根據事先定義好的type,傳給store.dispatch(action),觸發reducer方法,更新state。

    一般一個action是一個對象,相似於這樣

    {
        type: 'UPDATE_TEXT',
        text: 'update'
    }
    複製代碼

    須要dispatch更新的時候dispatch(action),就會傳給reducer(action),reducer函數根據具體的type返回state,store會更新state。

  • actionCreator
    顧名思義,action建立函數。

    function updateText(text) {
       return {
         type: 'UPDATE_TEXT',
         text, 
       }
    }複製代碼

    這樣看起來更獨立一些,當操做的時候咱們只須要把text傳給creator,就能夠獲得一個符合要求的action。全部相關的actionCreator都放到一個文件裏,放到一個對象裏actionCreators, 調用action的時候,只須要dispatch(actionCreators.updateText('abc'))。

bindActionCreators

這個函數接受兩個參數, (actionCreators, dispatch)。 第一個參數是actionCreator的集合,是一個對象。第二個參數dispatch由store提供。
這個函數一般是配合react-redux使用,經過connect把改造後的actions對象傳給Component。這樣,咱們就能夠在一個專門的actions.js文件裏定義他們,而咱們的組件內部,不須要寫dispatch(action)這樣的操做,組件內部感知不到redux的存在。這樣的好處是下降耦合性,組件內部只是調用一個方法,而這些方法經過connect傳給父組件,子組件仍舊是獨立的,或者說是木偶組件。

//actions.js
function updateText(text) {
return {
  type: 'UPDATE_TEXT',
  text
}
}
function addText(text) {
return {
   type: 'ADD_TEXT',
   text
}
}
const actions = { updateText, addText  }

import { bindActionCreators, createStore } from 'redux';
const store = createStore(reducer);
const bindActions = bindActionCreators(actions, store.dispatch)
store.actions = bindActions;

ReactDOM.render(
<App  store />
)
//僞代碼複製代碼




Redux 入門介紹 :https://github.com/creeperyang/blog/issues/32

Redux is a predictable state container for JavaScript apps.

Redux 是一個給JavaScript app使用的可預測的狀態容器。

爲何須要Redux?(動機)

JavaScript單頁應用愈來愈複雜,代碼必須管理遠比之前多的狀態(state)。這個狀態包括服務端返回數據,緩存數據,本地建立的數據(未同步到服務器);也包括UI狀態,如須要管理激活的路由,選中的標籤,是否顯示加載動效或者分頁器等等。

管理不斷變化的狀態是很難的。若是一個 model 能夠更新另外一個 model ,那麼一個 view 也能夠更新一個 model 並致使另外一個 model 更新,而後相應地,可能致使另外一個 view 更新 —— 你理不清你的 app 發生了什麼,失去了對 state 何時,爲何,怎麼變化的控制 。當系統變得 不透明和不肯定,就很難去重現 bug 和增長 feature 了。

經過 限制什麼時候以及怎麼更新,Redux 試圖讓 state 的變化能夠預測 。

這裏能夠配合閱讀 You Might Not Need Redux : Redux 的引入並不必定改善開發體驗,必須權衡它的限制與好處。

Redux自己很簡單,咱們下面首先闡述它的核心概念和三大原則。

核心概念

想象一下用普通 JavaScript 對象 來描述 app 的 state:

// 一個 todo app 的 state 多是這樣的:
{
  todos: [{
    text: 'Eat food',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }],
  visibilityFilter: 'SHOW_COMPLETED'
}複製代碼

這個對象就像沒有 setter 的 model,因此其它部分的代碼不能隨意修改它而形成難以復現的 bug 。

若是要改變 state ,咱們必須 dispatch 一個 action。action 是描述發生了什麼的普通 JavaScript 對象。

// 下面都是action:
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }複製代碼

強制 每一個 change 都必須用 action 來描述,可讓咱們清楚 app 里正在發生什麼, state 是爲何改變的。最後,把 state 和 actions 聯結起來,咱們須要 reducer 。

reducer 就是函數,以以前的 state 和 action 爲參數,返回新的 state :

// 關注 visibilityFilter 的 reducer
function visibilityFilter(state = 'SHOW_ALL', action) {
  if (action.type === 'SET_VISIBILITY_FILTER') {
    return action.filter;
  } else {
    return state;
  }
}

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action)
  };
}複製代碼

以上就是 Redux 的核心概念,注意到咱們並無用任何 Redux 的 API,沒加入任何 魔法。 Redux 裏有一些工具來簡化這種模式,可是主要的想法是描述如何根據這些 action 對象來更新 state。

三大原則

1. 單一數據源

整個應用的 state 被儲存在一棵 object tree 中,而且這個 object tree 只存在於惟一一個 store 中。

console.log(store.getState())

/* Prints
{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}
*/複製代碼

2. State 是隻讀的

改變 state 的惟一方式是觸發 (emit) action,action 是描述發生了什麼的對象。
這確保了視圖和網絡請求等都不能直接修改 state,相反它們只能表達想要修改的意圖。由於全部的修改都被集中化處理,且嚴格按照一個接一個的順序執行,所以不用擔憂 race condition 的出現。

store.dispatch({
  type: 'COMPLETE_TODO',
  index: 1
})

store.dispatch({
  type: 'SET_VISIBILITY_FILTER',
  filter: 'SHOW_COMPLETED'
})複製代碼

3. 使用純函數來執行修改

爲描述 action 怎麼改變 state tree,你要編寫 reducers。

Reducer 只是一些純函數,它接收以前的 state 和 action,並返回新的 state。剛開始你可能只須要一個 reducer ,但隨着應用變大,你會須要拆分 reducer 。

以 todo app 爲例迅速上手 Redux

1. 定義 actions

Action 就是把數據從應用(這些數據有多是服務器響應,用戶輸入或其它非 view 的數據)發送到 store 的有效載荷。 它是 store 數據的惟一來源,你經過 store.dispatch(action) 來發送它到 store。

添加新 todo 任務的 action 是這樣的:

const ADD_TODO = 'ADD_TODO'

{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}複製代碼

Action 本質上是 JavaScript 普通對象。Action 必須有一個字符串類型的 type 字段來表示將要執行的動做。多數狀況下,type 會被定義成字符串常量。當應用規模愈來愈大時,建議使用單獨的模塊/文件來存放 action。

除了 type 字段外,action 對象的結構徹底由你本身決定。但一般,咱們但願減小 action 中傳遞的數據。

Action 建立函數 (action creator)

Action 建立函數 就是生成 action 的方法。「action」 和 「action 建立函數」 這兩個概念很容易混在一塊兒,使用時最好注意區分。

// 生成一個 ADD_TODO 類型的 action
function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}複製代碼

2. Reducers

Action 只是描述了有事情發生了這一事實,並無指明應用如何更新 state。而這正是 reducer 要作的事情。

設計 State 結構

在 Redux 應用中,全部的 state 都被保存在一個單一對象中。最好能夠在寫代碼以前想好 state tree 應該是什麼形狀的。

一般,這個 state tree 須要存放一些數據,以及一些 UI 相關的 state。這樣作沒問題,但儘可能把數據與 UI 相關的 state 分開。

// todo app 的 state
{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}複製代碼

處理 Action

有了 state 結構後,咱們能夠來寫 reducer 了。 reducer 就是一個純函數,接收舊的 state 和 action,返回新的 state。

(previousState, action) => newState複製代碼

保持 reducer 純淨很是重要。永遠不要在 reducer 裏作這些操做:

  • 修改傳入參數;
  • 執行有反作用的操做,如 API 請求和路由跳轉;
  • 調用非純函數,如 Date.now()Math.random()

在高級篇裏會介紹如何執行有反作用的操做。如今只須要記住 reducer 必定要保持純淨。只要傳入參數相同,返回計算獲得的下一個 state 就必定相同。沒有特殊狀況、沒有反作用,沒有 API 請求、沒有變量修改,單純執行計算。

import { 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
          }
        ]
      })
    case TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: state.todos.map((todo, index) => {
          if(index === action.index) {
            return Object.assign({}, todo, {
              completed: !todo.completed
            })
          }
          return todo
        })
      })
    default:
      return state
  }
}複製代碼

注意:

  • 不要修改 state。 使用 Object.assign({}, ...) 新建了一個副本。
  • default 狀況下返回舊的 state。 遇到未知的 action 時,必定要返回舊的 state。

咱們看到,多個 action 下,reducer 開始變得複雜。是否能夠更通俗易懂?這裏的 todosvisibilityFilter 的更新看起來是相互獨立的,咱們能夠嘗試拆分到單獨的函數裏。

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 todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
    case TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: todos(state.todos, action)
      })
    default:
      return state
  }
}複製代碼

注意 todos 依舊接收 state,但它變成了一個數組!如今 todoApp 只把須要更新的一部分 state 傳給 todos 函數,todos 函數本身肯定如何更新這部分數據。這就是所謂的 reducer 合成,它是開發 Redux 應用最基礎的模式。

如今更進一步,把 visibilityFilter 獨立出去。那麼咱們能夠有個主 reducer,它調用多個子 reducer 分別處理 state 中的一部分數據,而後再把這些數據合成一個大的單一對象。主 reducer 再也不須要知道完整的 initial state。初始時,若是傳入 undefined, 子 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 只負責管理全局 state 中它負責的一部分。每一個 reducer 的 state 參數都不一樣,分別對應它管理的那部分 state 數據。

當應用愈來愈複雜,咱們還能夠將拆分後的 reducer 放到不一樣的文件中, 以保持其獨立性並用於專門處理不一樣的數據域。

最後,Redux 提供了 combineReducers() 工具來作上面 todoApp 作的事情。能夠用它這樣重構 todoApp:

import { combineReducers } from 'redux';

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

// 徹底等價於
function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}複製代碼

3. 建立 store

前面兩小節中,咱們學會了使用 action 來描述「發生了什麼」,和使用 reducers 來根據 action 更新 state 的用法。

Store 就是把它們聯繫到一塊兒的對象。Store 有如下職責:

  • 維持應用的 state;
  • 提供 getState() 方法獲取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 經過 subscribe(listener) 註冊監聽器;
  • 經過 subscribe(listener) 返回的函數註銷監聽器。

再次強調一下 Redux 應用只有一個 單一 的 store。當須要拆分數據處理邏輯時,你應該使用 reducer 組合 而不是建立多個 store。

根據已有的 reducer 來建立 store 是很是容易的。在前面咱們使用 combineReducers() 將多個 reducer 合併成爲一個。如今咱們將其導入,並傳給 createStore()

import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)複製代碼

你能夠把初始狀態 intialState 做爲第二個參數傳給 createStore()。這對開發同構應用時很是有用,服務器端 redux 應用的 state 結構能夠與客戶端保持一致, 那麼客戶端能夠將從網絡接收到的服務端 state 直接用於本地數據初始化。

let store = createStore(todoApp, window.STATE_FROM_SERVER)複製代碼

發起 actions

如今咱們已經建立好了 store ,能夠驗證一下:

import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions'

// 打印初始狀態
console.log(store.getState())

// 每次 state 更新時,打印日誌
// 注意 subscribe() 返回一個函數用來註銷監聽器
let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)

// 發起一系列 action
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))

// 中止監聽 state 更新
unsubscribe();複製代碼

4. 數據流

嚴格的單向數據流 是 Redux 架構的設計核心。

這意味着應用中全部的數據都遵循相同的生命週期,這樣可讓應用變得更加可預測且容易理解。同時也鼓勵作數據範式化,這樣能夠避免使用多個且獨立的沒法相互引用的重複數據。

Redux 應用中數據的生命週期遵循下面 4 個步驟:

  1. 調用 store.dispatch(action)

  2. Redux store 調用傳入的 reducer 函數。

  3. 根 reducer 應該把多個子 reducer 輸出合併成一個單一的 state 樹。

  4. Redux store 保存了根 reducer 返回的完整 state 樹。

這個新的樹就是應用的下一個 state!全部訂閱 store.subscribe(listener) 的監聽器都將被調用;監聽器裏能夠調用 store.getState() 得到當前 state。

搭配 React 一塊兒使用

首先強調一下:Redux 和 React 之間沒有關係。Redux 支持 React、Angular、Ember、jQuery 甚至純 JavaScript。

儘管如此,Redux 仍是和 React 和 Deku 這類框架搭配起來用最好,由於這類框架容許你以 state 函數的形式來描述界面,Redux 經過 action 的形式來發起 state 變化。

安裝 react-redux

Redux 自身並不包含對 React 的綁定庫,咱們須要單獨安裝 react-redux

Presentational and Container Components

綁定庫是基於 容器組件和展現組件相分離 的開發思想。建議先讀完這篇文章。

技術上講,咱們能夠手動用 store.subscribe() 來編寫容器組件,但這就沒法使用 React Redux 作的大量性能優化了。通常使用 React Redux 的 connect() 方法來生成容器組件。(沒必要爲了性能而手動實現 shouldComponentUpdate 方法)

設計組件層次結構

還記得前面 設計 state 根對象的結構 嗎?如今就要定義與它匹配的界面的層次結構。這不是 Redux 相關的工做,React 開發思想在這方面解釋的很是棒。

  1. 展現組件: 純粹的UI組件,定義外觀而不關心數據怎麼來,怎麼變。傳入什麼就渲染什麼。

  2. 容器組件: 把展現組件鏈接到 Redux。監聽 Redux store 變化並處理如何過濾出要顯示的數據。

  3. 其它組件 有時很難分清到底該使用容器組件仍是展現組件,而且組件並不複雜,這時能夠混合使用。

實現組件

省略其它部分,主要講講容器組件通常怎麼寫。

import { connect } from 'react-redux'

// 3. connect 生成 容器組件
const ContainerComponent = connect(
  mapStateToProps,
  mapDispatchToProps
)(PresentationalComponent)

// 2. mapStateToProps 指定如何把當前 Redux store state 映射到展現組件的 props 中
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)
  }
}

const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

// 1. mapDispatchToProps() 方法接收 dispatch() 方法並返回指望注入到展現組件的 props 中的回調方法。
const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}
// 可使用 Redux 的 bindActionCreators 把全部的暴露出來的 actionCreators 轉成方法注入 props

export default ContainerComponent複製代碼

connect 自己仍是很明確的,指定咱們注入哪些 data 和 function 到展現組件的 props ,給展現組件使用。



API 探索

Redux API

1. createStore(reducer, [preloadedState], enhancer)

建立一個 Redux store 來以存放應用中全部的 state。詳情可見 Redux API,這裏主要強調兩點:

  1. preloadedState:初始時的 state。在同構中會用到,好比從一個session恢復數據。

當 store 建立後,Redux 會 dispatch action({ type: ActionTypes.INIT })) 到 reducer 上,獲得初始的 state 來填充 store。因此你的初始 state 是 preloadedState 在 reducers 處理 ActionTypes.INIT action 後的結果。 github.com/reactjs/red…

  1. enhancer:若是有 enhancer,那麼會首先獲得加強的 createStore,而後再createStore(reducer, [preloadedState])

github.com/reactjs/red… 可結合下面 applyMiddleware一塊兒看。

2. middleware 與 applyMiddleware(...middlewares)

咱們能夠用 middleware 來擴展 Redux。Middleware 可讓你包裝 store 的 dispatch 方法來達到你想要的目的。同時, middleware 還擁有「可組合」這一關鍵特性。多個 middleware 能夠被組合到一塊兒使用,造成 middleware 鏈。其中,每一個 middleware 都不須要關心鏈中它先後的 middleware 的任何信息。

middleware 的函數簽名是 ({ getState, dispatch }) => next => action

以下是兩個 middleware:

// logger middleware
function logger({ getState }) {
  return (next) => (action) => {
    console.log('will dispatch', action)

    // 調用 middleware 鏈中下一個 middleware 的 dispatch。
    let returnValue = next(action)

    console.log('state after dispatch', getState())

    // 通常會是 action 自己,除非
    // 後面的 middleware 修改了它。
    return returnValue
  }
}

// thunk middleware
function thunk({ dispatch, getState }) {
  return (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState);
    }

    return next(action);
  }
}複製代碼

applyMiddleware 返回一個應用了 middleware 後的 store enhancer。這個 store enhancer 的簽名是 createStore => createStore,可是最簡單的使用方法就是直接做爲最後一個 enhancer 參數傳遞給 createStore() 函數。

再來看下 applyMiddleware

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 是 [(next) => (action) => action, ...]
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    // compose(...chain) 返回這樣一個函數:
    // 對 chain 進行 reduce,從右向左執行,每次的結果做爲下次執行的輸入
    dispatch = compose(...chain)(store.dispatch)
    // 最終的 dispatch 是這樣的:(action) => action

    return {
      ...store,
      dispatch
    }
  }
}複製代碼

能夠看到(假設 enhancerTest = applyMiddleware(A, B, C)):

  1. middleware 其實只是劫持/包裝了 dispatch
  2. dispatch 本質上是同步的,但咱們能夠經過 thunk 等延遲執行 dispatch
  3. chain[index](dispatch) --> (action) => action,即咱們獲得的 dispatch 是一個層層嵌套的 (action) => action 函數。
  4. 除了最右側的 C 獲得的 next 是本來的 dispatch,剩下的都是被層層嵌套的 (action) => action 函數,而且越右側越嵌套在裏面,因此當 dispatch(action) 調用時,將會如下面順序執行:A -> B -> C -> B -> AC 以前 A/B 都只執行了 next 以前的邏輯,以後各自徹底執行。

3. combineReducers(reducers)

把一個由多個不一樣 reducer 函數做爲 value 的 object,合併成一個最終的 reducer 函數。

真的很簡單,從邏輯上來說,就是:

combineReducers({
  keyA: reducerA,
  keyB: reducerB
})

// --->

function recuderAll (prevState, action) {
  return {
    keyA: reducerA(prevState.keyA, action),
    keyB: reducerB(prevState.keyB, action)
  }
}複製代碼

核心就是幹了上面的事,只是多了一些判斷和檢查。

4. bindActionCreators(actionCreators, dispatch)

把 action creators 轉成擁有同名 keys 的對象,但使用 dispatch 把每一個 action creator 包圍起來,這樣能夠直接調用它們。

const actionCreators = {
  updateOrAddFilter(filter) {
    type: UPDATE_OR_ADD_FILTER,
    filter
  },
  removeFilter(type) {
    type: REMOVE_FILTER,
    filterType: type
  }
}

bindActionCreators(actionCreators, dispatch)

// -->

{
  updateOrAddFilter: (...args) => dispatch(original_updateOrAddFilter(...args)),
  removeFilter: (...args) => dispatch(original_removeFilter(...args)),
}複製代碼

核心就是自動 dispatch ,這樣咱們能夠在 react 組件裏直接調用,Redux store 就能收到 action。

React Redux API

1. Provider

用法:

ReactDOM.render(
  <Provider store={store}>
    <MyRootComponent />
  </Provider>,
  rootEl
)複製代碼

源碼:

// storeKey 默認是 'store'
class Provider extends Component {
    getChildContext() {
      return { [storeKey]: this[storeKey], [subscriptionKey]: null }
    }

    constructor(props, context) {
      super(props, context)
      this[storeKey] = props.store;
    }

    render() {
      return Children.only(this.props.children)
    }
}
Provider.propTypes = {
    store: storeShape.isRequired,
    children: PropTypes.element.isRequired,
}
Provider.childContextTypes = {
    [storeKey]: storeShape.isRequired,
    [subscriptionKey]: subscriptionShape,
}複製代碼

注意到, Provider 應用了 React Context,子組件均可以去訪問 store

2. connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

connect 的函數簽名是 ([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) => (WrappedComponent) => ConnectComponent,最後返回的 onnectComponent 能夠經過 context 去訪問 store

connect API 比較複雜,這裏主要講下前兩個參數。

  • [mapStateToProps(state, [ownProps]): stateProps] (Function): 若是定義該參數,組件將會監聽 Redux store 的變化。任什麼時候候,只要 Redux store 發生改變,mapStateToProps 函數就會被調用。該回調函數必須返回一個純對象,這個對象會與組件的 props 合併。若是你省略了這個參數,你的組件將不會監聽 Redux store。
  • [mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function): 若是傳遞的是一個對象,那麼每一個定義在該對象的函數都將被看成 Redux action creator,並且這個對象會與 Redux store 綁定在一塊兒,其中所定義的方法名將做爲屬性名,合併到組件的 props 中。
相關文章
相關標籤/搜索