Redux初探與異步數據流


基本認知

先貼一張redux的基本結構圖
redux
原圖來自《UNIDIRECTIONAL USER INTERFACE ARCHITECTURES》

在這張圖中,我們可以很清晰的看到,view中產生action,通過store.dispatch(action)將action交由reducer處理,最終根據處理的結果更新view。
在這個過程中,action是簡單對象,用於描述一個動作以及對應於該動作的數據。例如:

const ADD_TODO = 'ADD_TODO';

// action
{
	type: ADD_TODO,
	data: 'some data'
}  

而reducer則是純函數,且是冪等的,即只要傳入參數相同,返回計算得到的下一個 state 就一定相同。沒有特殊情況、沒有副作用,沒有 API 請求、沒有變量修改,單純執行計算。

同步數據流

在擁有了以上基本認知之后,我們來看下redux到底是如何工作的。Talk is cheap, show me the code.

import React from 'react'  
import { createStore, bindActionCreators } from 'redux'
import { connect } from 'react-redux' 
import ReactDom from 'react-dom'
import { Provider } from 'react-redux'

function createAction() {
  return {
  	type: 'ADD_TODO',
  	data: 'some data'
  }
}  

class App extends React.Component {
	constructor() {
		super();
	}
	render() {
		return (
			<div style={{width:'200px', height:'200px',margin:'100px',border:'2px solid black'}}>
				<div onClick={this.props.actions.createAction.bind(this)}>
				  {"Click Me!"}
				</div>
			</div>
		);
	}
}

function mapStateToProps(state) {
  return {
    data: state
  }
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators({createAction}, dispatch)
  }
}

var AppApp = connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

function reducer(state, action) {
  console.log(action);
  return state;
}

var store = createStore(reducer);
ReactDom.render(
  <Provider store={store}>
    <AppApp />
  </Provider>,
  document.getElementById('container')
);

這是一個精簡版本的redux demo,每點擊一次“Click Me!”,控制台會打印一次action。

由於篇幅限制,以上代碼未分模塊

下面是截圖:
效果圖

控制台打印輸出:
控制台打印

從上面代碼中可以清晰的看出,當用戶點擊“Click Me!”的時候,會立即調用createAction產生一個action,之后redux獲取這個action並調用store.dispatch將這個action丟給reducer進行處理,demo中的reducer僅僅打印了action。
數據從view中流出,經reducer處理后又回到了view。
至此,我們看到的一切都是跟上面的基本認知是一致的。

接下來說說異步數據流,這塊也是困擾了我好久,直到最近才搞清楚內在原因。

Redux Middleware

redux為我們做了很多的事情,我們都可以不用通過顯示的調用dispatch函數就將我們的action傳遞給reducer。這在前面的demo中就可以看到。但是至此,redux一直沒有解決異步的問題。試想,如果我在頁面輸入一段內容,然后觸發了一個搜索動作,此時需要向服務端請求數據並將返回的數據展示出來。這是一個很常見的功能,但是涉及到異步請求,剛剛的demo中的方法已經不再適用了。那么redux是如何解決異步問題的呢?

沒錯,就是引入middleware。middleware,顧名思義就是中間件。用過express的同學對中間件應該都很熟悉。其實在redux中,middleware並不僅僅用於解決異步的問題,它還可以做很多其他的事情,比如記錄日志、錯誤報告、路由等等。

關於redux middleware的說明在官方文檔中已經有了非常清晰的說明,中文版英文版都有,這里就不在贅述,只摘錄一句話,說明如下。

It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

這里我想說下redux middleware的具體實現,我也正是從源代碼中找到了困擾我的問題的原因。
先看applyMiddleware(...middlewares)的代碼:

import compose from './compose'
export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, initialState, enhancer) => {
    var store = createStore(reducer, initialState, 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
    }
  }
}  

代碼很短,此處我們只關注最內層函數的實現。在創建了store以后,我們對傳進來的每一個middleware進行如下處理:

	var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))

處理后得到一個數組保存在chain中。之后將chain傳給compose,並將store.dispatch傳給返回的函數。那么在這里面做了什么呢?我們再看compose的實現:

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  } else {
    const last = funcs[funcs.length - 1]
    const rest = funcs.slice(0, -1)
    return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
  }
}

compose中的核心動作就是將傳進來的所有函數倒序(reduceRight)進行如下處理:

(composed, f) => f(composed)

我們知道Array.prototype.reduceRight是從右向左累計計算的,會將上一次的計算結果作為本次計算的輸入。再看看applyMiddleware中的調用代碼:

dispatch = compose(...chain)(store.dispatch)

compose函數最終返回的函數被作為了dispatch函數,結合官方文檔和代碼,不難得出,中間件的定義形式為:

function middleware({dispatch, getState}) {
	return function (next) {
		return function (action) {
			return next(action);
		}
	}
}

或  

middleware = (dispatch, getState) => next => action => {
	next(action);
}

也就是說,redux的中間件是一個函數,該函數接收dispatch和getState作為參數,返回一個以dispatch為參數的函數,這個函數的返回值是接收action為參數的函數(可以看做另一個dispatch函數)。在中間件鏈中,以dispatch為參數的函數的返回值將作為下一個中間件(准確的說應該是返回值)的參數,下一個中間件將它的返回值接着往下一個中間件傳遞,最終實現了store.dispatch在中間件間的傳遞。

看了中間件的文檔和代碼之后,我算是搞明白了中間件的原理。之前一直困擾我的問題現在看來其實是概念問題(此處不提也罷),中間件只關注dispatch函數的傳遞,至於在傳遞的過程中干了什么中間件並不關心。

下面看看通過中間件,我們如何實現異步調用。這里就不得不提redux-thunk中間件了。

redux-thunk

redux與redux-thunk是同一個作者。

我們知道,異步調用什么時候返回前端是無法控制的。對於redux這條嚴密的數據流來說,如何才能做到異步呢。redux-thunk的基本思想就是通過函數來封裝異步請求,也就是說在actionCreater中返回一個函數,在這個函數中進行異步調用。我們已經知道,redux中間件只關注dispatch函數的傳遞,而且redux也不關心dispatch函數的返回值,所以只需要讓redux認識這個函數就可以了。
看了一下redux-thunk的源碼:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return next(action);
  };
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;

這段代碼跟上面我們看到的中間件沒有太大的差別,唯一一點就是對action做了一下如下判斷:

	if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

也就是說,如果發現actionCreater傳過來的action是一個函數的話,會執行一下這個函數,並以這個函數的返回值作為返回值。前面已經說過,redux對dispatch函數的返回值不是很關心,因此此處也就無所謂了。

這樣的話,在我們的actionCreater中,我們就可以做任何的異步調用了,並且返回任何值也無所謂,所以我們可以使用promise了:

function actionCreate() {
	return function (dispatch, getState) {
		// 返回的函數體內自由實現。。。
		Ajax.fetch({xxx}).then(function (json) {
			dispatch(json);
		})
	}
}

通過redux-thunk,我們將異步的操作融合進了現有的數據流中。
最后還需要注意一點,由於中間件只關心dispatch的傳遞,並不限制你做其他的事情,因此我們最好將redux-thunk放到中間件列表的首位,防止其他中間件中返回異步請求。

小結

以上是最近一段時間學習和思考的總結。在這期間發現,學習新知識的基礎是要把概念理解清楚,不能一味的看樣例跑demo,不理解概念對demo也只是知其然不知其所以然,很容易陷入一些通過樣例代碼理解出來的錯誤的概念中,后面再糾正就需要花費很多時間和精力了!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM