react-redux簡介
redux是一個資料管理框架,而react-redux是專門針對react開發的一個外掛。react-redux提供了2個API,Provider和connect。本來打算在一篇文章同時講解2個API的實現,不過看了一下connect的原始碼,368行,還是分開解析吧。
本文帶領大家分析Provider的核心程式碼。
如何使用Provider
我們先了解在react專案中是如何使用Provider。
import { Provider } from 'react-redux';
import configureStore from './store/configureStore';
const store = configureStore();
ReactDOM.render((
<Provider store={store}>
</Provider>),
document.getElementById('root')
);複製程式碼
上面的程式碼可以看出,使用Provider分為下面幾個步驟:
1、匯入Provider 這裡跟小白分享一個小知識,你可以看到Provider加了個大括號,而第二個import configureStore沒有加大括號,這是因為react-redux的檔案中沒有指定default輸出。如果指定了export default,則不需要加大括號,注意一個js檔案只能有一個default。
import { Provider } from 'react-redux';複製程式碼
2、將store作為引數傳入Provider。
<Provider store={store}>
</Provider>複製程式碼
Provider原始碼
import { Component, Children } from 'react'
import PropTypes from 'prop-types'
import storeShape from '../utils/storeShape'
import warning from '../utils/warning'
let didWarnAboutReceivingStore = false
function warnAboutReceivingStore() {
if (didWarnAboutReceivingStore) {
return
}
didWarnAboutReceivingStore = true
warning(
'<Provider> does not support changing `store` on the fly. ' +
'It is most likely that you see this error because you updated to ' +
'Redux 2.x and React Redux 2.x which no longer hot reload reducers ' +
'automatically. See https://github.com/reactjs/react-redux/releases/' +
'tag/v2.0.0 for the migration instructions.'
)
}
export default class Provider extends Component {
getChildContext() {
return { store: this.store }
}
constructor(props, context) {
super(props, context)
this.store = props.store
}
render() {
return Children.only(this.props.children)
}
}
if (process.env.NODE_ENV !== 'production') {
Provider.prototype.componentWillReceiveProps = function (nextProps) {
const { store } = this
const { store: nextStore } = nextProps
if (store !== nextStore) {
warnAboutReceivingStore()
}
}
}
Provider.propTypes = {
store: storeShape.isRequired,
children: PropTypes.element.isRequired
}
Provider.childContextTypes = {
store: storeShape.isRequired
}複製程式碼
Provider原始碼解析
Provider只有一個引數,非常簡單,程式碼也僅有55行。
1、Provider是一個react元件
import { Component, Children } from 'react'
import PropTypes from 'prop-types'
import storeShape from '../utils/storeShape'
import warning from '../utils/warning'
export default class Provider extends Component {
getChildContext() {
return { store: this.store }
}
constructor(props, context) {
super(props, context)
this.store = props.store
}
render() {
return Children.only(this.props.children)
}
}複製程式碼
Provider元件寫了3個方法,getChildContext、constructor、render。
constructor是構造方法,this.store = props.store中的this表示當前的元件。在建構函式定義this.store的作用是為了能夠在getChildContext方法中讀取到store。
你最不熟悉的可能就是getChildContext,翻譯過來就是上下文。什麼意思呢?又有什麼用呢?我們看到getChildContext方法是返回store。接著,就看不到store哪去了。
最後執行render渲染,返回一個react子元素。Children.only是react提供的方法,this.props.children表示的是隻有一個root的元素。
2、給Provider元件設定propTypes驗證。storeShape是一個封裝的方法。
Provider.propTypes = {
store: storeShape.isRequired,
children: PropTypes.element.isRequired
}
//storeShape
import PropTypes from 'prop-types'
export default PropTypes.shape({
subscribe: PropTypes.func.isRequired,
dispatch: PropTypes.func.isRequired,
getState: PropTypes.func.isRequired
})複製程式碼
3、驗證childContextTypes 它的作用就是讓Provider下面的子元件能夠訪問到store。 詳細解釋和用法看 react關於context的介紹
Provider.childContextTypes = {
store: storeShape.isRequired
}複製程式碼
4、node執行環境判斷 如果不是生產環境,也就是在開發環境中,實現componentWillReceiveProps()。
if (process.env.NODE_ENV !== 'production') {
Provider.prototype.componentWillReceiveProps = function (nextProps) {
const { store } = this
const { store: nextStore } = nextProps
if (store !== nextStore) {
warnAboutReceivingStore()
}
}
}複製程式碼
其實也可以把這段程式碼寫到Provider元件內部去。
他的作用是當接收到新的props的時候,如果是在開發環境下,就判斷當前的store和下一個store是不是不相等,如果是,就執行warnAboutReceivingStore()。
export default class Provider extends Component {
componentWillReceiveProps(nextProps) {
if (process.env.NODE_ENV !== 'production') {
const { store } = this
const { store: nextStore } = nextProps
if (store !== nextStore) {
warnAboutReceivingStore()
}
}
}
render() {
return Children.only(this.props.children)
}
}複製程式碼
5、warnAboutReceivingStore的作用。 上面說到執行了warnAboutReceivingStore,那麼warnAboutReceivingStore的作用是什麼呢?
let didWarnAboutReceivingStore = false
function warnAboutReceivingStore() {
if (didWarnAboutReceivingStore) {
return
}
didWarnAboutReceivingStore = true
warning(
'<Provider> does not support changing `store` on the fly. ' +
'It is most likely that you see this error because you updated to ' +
'Redux 2.x and React Redux 2.x which no longer hot reload reducers ' +
'automatically. See https://github.com/reactjs/react-redux/releases/' +
'tag/v2.0.0 for the migration instructions.'
)複製程式碼
didWarnAboutReceivingStore是一個開關的作用,預設是false,也就是不執行warning操作。當props更新的時候,執行了warnAboutReceivingStore(),如果didWarnAboutReceivingStore為true,則return,否則就將didWarnAboutReceivingStore設定為true。然後就會執行warning的警告機制。
這樣做的目的是不允許在componentWillReceiveProps做store的更新操作。
總結
很快就到尾聲了,Provider是一個react元件,提供了一個引數store,然後渲染了一個子元件,我們通常把路由渲染成子元件,最後還處理了一個異常情況,提供了warning提示。
大部分時候是這樣用的。在react-router4中,也支援這種寫法,Provider也可以直接巢狀在自定義的react元件中。
<Provider store={store}>
<Router history={hashHistory}>
{routes}
</Router>
</Provider>複製程式碼
在redux的配置檔案中,如果你使用了redux-logger,也許你會寫下面這樣一段程式碼:
import thunk from 'redux-thunk';
import promise from 'redux-promise';
import createLogger from 'redux-logger';
const logger = createLogger();
const createStoreWithMiddleware = applyMiddleware(thunk, promise, logger)(createStore);
const store = createStoreWithMiddleware(reducer);複製程式碼
現在,我們只關注redux-logger,我們可以看到使用redux-logger分為下面幾個步驟:
1、匯入redux-logger
import createLogger from 'redux-logger';複製程式碼
2、執行createLogger方法,將返回結果賦值給常量
const logger = createLogger();複製程式碼
3、將looger傳入applyMiddleware()
applyMiddleware(logger)複製程式碼
有2個難點,第一是createLogger()的返回值到底是如何實現的。第二就是applyMiddleware方法如何處理返回值。因為本文是講redux-logger的實現,所以我們只分析createLogger()
redux-logger中createLogger方法原始碼
const repeat = (str, times) => (new Array(times + 1)).join(str);
const pad = (num, maxLength) => repeat(`0`, maxLength - num.toString().length) + num;
//使用新的效能API可以獲得更好的精度(如果可用)
const timer = typeof performance !== `undefined` && typeof performance.now === `function` ? performance : Date;
function createLogger(options = {}) {
return ({ getState }) => (next) => (action) => {
const {
level, //級別
logger, //console的API
collapsed, //
predicate, //logger的條件
duration = false, //列印每個action的持續時間
timestamp = true, //列印每個action的時間戳
transformer = state => state, //在列印之前轉換state
actionTransformer = actn => actn, //在列印之前轉換action
} = options;
const console = logger || window.console;
// 如果控制檯未定義則退出
if (typeof console === `undefined`) {
return next(action);
}
// 如果謂詞函式返回false,則退出
if (typeof predicate === `function` && !predicate(getState, action)) {
return next(action);
}
const started = timer.now();
const prevState = transformer(getState());
const returnValue = next(action);
const took = timer.now() - started;
const nextState = transformer(getState());
// 格式化
const time = new Date();
const isCollapsed = (typeof collapsed === `function`) ? collapsed(getState, action) : collapsed;
const formattedTime = timestamp ? ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}` : ``;
const formattedDuration = duration ? ` in ${took.toFixed(2)} ms` : ``;
const formattedAction = actionTransformer(action);
const message = `action ${formattedAction.type}${formattedTime}${formattedDuration}`;
const startMessage = isCollapsed ? console.groupCollapsed : console.group;
// 渲染
try {
startMessage.call(console, message);
} catch (e) {
console.log(message);
}
if (level) {
console[level](`%c prev state`, `color: #9E9E9E; font-weight: bold`, prevState);
console[level](`%c action`, `color: #03A9F4; font-weight: bold`, formattedAction);
console[level](`%c next state`, `color: #4CAF50; font-weight: bold`, nextState);
} else {
console.log(`%c prev state`, `color: #9E9E9E; font-weight: bold`, prevState);
console.log(`%c action`, `color: #03A9F4; font-weight: bold`, formattedAction);
console.log(`%c next state`, `color: #4CAF50; font-weight: bold`, nextState);
}
try {
console.groupEnd();
} catch (e) {
console.log(`—— log end ——`);
}
return returnValue;
};
}
export default createLogger;複製程式碼
解析redux-logger
1、入口函式createLogger(options = {}) 我們在redux配置檔案中呼叫的就是這個函式,也是redux-logger中唯一一個函式,它只有一個引數option,option是object。
2、return ({ getState }) => (next) => (action) => {} 這行程式碼看起來很複雜,一堆的箭頭函式,其實很簡單,createLogger()一定會有一個返回值,但是,我們在控制檯列印action資訊的時候,需要獲取state和action的資訊,所以,首先傳入getState方法,getState是redux提供的一個方法,用來獲取store的state。然後再傳入next方法,接著傳入action方法。next和action都是redux提供的方法,到這一步,我們就把需要的引數都傳入到函式中,可以進行下一步操作了。
3、定義option的配置引數 我們在使用redux-logger的時候,習慣了不配置任何引數,直接呼叫createLogger(),使用預設的配置。但其實還可以手動傳入一個option配置,不過並不常用。
const {
level, //級別
logger, //console的API
collapsed, //
predicate, //logger的條件
duration = false, //列印每個action的持續時間
timestamp = true, //列印每個action的時間戳
transformer = state => state, //在列印之前轉換state
actionTransformer = actn => actn, //在列印之前轉換action
} = options;複製程式碼
4、定義console 如果你給option配置了console相關的API,那麼就使用你的配置,如果沒有配置,就使用window.console
const console = logger || window.console;複製程式碼
5、新增2個異常情況做退出處理 第一個if語句是控制檯未定義就返回下一個action操作,但是我想不到在瀏覽器中會出現console方法不存在的情況。 第二個if語句的predicate表示warn、log、error等屬於console的方法。&&表示2個條件要同時滿足才執行下面的操作。predicate(getState, action)其實就是類似console.log(getState, action)
// 如果控制檯未定義則退出
if (typeof console === `undefined`) {
return next(action);
}
// 如果謂詞函式返回false,則退出
if (typeof predicate === `function` && !predicate(getState, action)) {
return next(action);
}複製程式碼
6、給各個常量賦值 為什麼會有這麼多常量呢?我們來看一張圖,圖上展示了需要列印的各種資訊。
總結出來就是:
action action.type @ timer
prev state {}
action {}
next state {}
這裡需要的是action.type, timer, 各種狀態下的state
const started = timer.now();
const prevState = transformer(getState());
const returnValue = next(action);
const took = timer.now() - started;
const nextState = transformer(getState());
// 格式化
const time = new Date();
const isCollapsed = (typeof collapsed === `function`) ? collapsed(getState, action) : collapsed;
const formattedTime = timestamp ? ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}` : ``;
const formattedDuration = duration ? ` in ${took.toFixed(2)} ms` : ``;
const formattedAction = actionTransformer(action);
const message = `action ${formattedAction.type}${formattedTime}${formattedDuration}`;
const startMessage = isCollapsed ? console.groupCollapsed : console.group;複製程式碼
上面程式碼資訊量比較大,我們還可以拆分出來看看。
a、先獲取一個開始時間started,然後讀取state,這個state是之前的狀態prevState。returnValue是返回值,返回下一個action。took是你執行完前面3行程式碼之後的真實時間,在這裡因為沒有用到非同步處理,所以我暫且認為transformer()和next()是同步的。nextState是新的state。
這段程式碼歸納起來看就是先讀取開始時間,然後讀取state,這個state因為還有更新action,所以是舊的state,然後執行next傳入新的action,更新完成之後,獲取結束時間,計算更新action的時間差,然後再獲取更新後的state。
const started = timer.now();
const prevState = transformer(getState());
const returnValue = next(action);
const took = timer.now() - started;
const nextState = transformer(getState());複製程式碼
b、下面的程式碼做了一件事情,設定列印的資訊。
formattedTime是列印出來的時間,格式是 時:分:秒,formattedDuration是時間差,formattedAction是當前的action方法。isCollapsed用處不大,不管他。
// 格式化
const time = new Date();
const isCollapsed = (typeof collapsed === `function`) ? collapsed(getState, action) : collapsed;
const formattedTime = timestamp ? ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}` : ``;
const formattedDuration = duration ? ` in ${took.toFixed(2)} ms` : ``;
const formattedAction = actionTransformer(action);
const message = `action ${formattedAction.type}${formattedTime}${formattedDuration}`;
const startMessage = isCollapsed ? console.groupCollapsed : console.group;複製程式碼
這幾行程式碼做的事情也非常簡單,給需要列印的常量賦值。然後組合之後賦值給message:
const message = `action ${formattedAction.type}${formattedTime}${formattedDuration}`;複製程式碼
message == action action.type @ time
7、try {} catch() {} 部分一般不會用到,也可以不管。
startMessage.call(console, message);表示將message當做引數傳入startMessage,call的第一個引數是指執行環境,意思就是在console列印message資訊。
try {
startMessage.call(console, message);
} catch (e) {
console.log(message);
}複製程式碼
8、列印console的資訊,這就圖上列印出來的部分了。
因為我們通常沒有配置level,所以執行的是else語句的操作。
if (level) {
console[level](`%c prev state`, `color: #9E9E9E; font-weight: bold`, prevState);
console[level](`%c action`, `color: #03A9F4; font-weight: bold`, formattedAction);
console[level](`%c next state`, `color: #4CAF50; font-weight: bold`, nextState);
} else {
console.log(`%c prev state`, `color: #9E9E9E; font-weight: bold`, prevState);
console.log(`%c action`, `color: #03A9F4; font-weight: bold`, formattedAction);
console.log(`%c next state`, `color: #4CAF50; font-weight: bold`, nextState);
}複製程式碼
9、遊戲結束
try {
console.groupEnd();
} catch (e) {
console.log(`—— log end ——`);
}複製程式碼
10、返回值
return returnValue;複製程式碼
總結
redux-logger做的事情是在控制檯輸出action的資訊,所以首先要獲取前一個action,當前action,然後是下一個action。看完之後,你對redux-logger原始碼的理解加深了嗎?
在react開發中,一部分人使用redux-thunk,一部分人使用redux-saga,彼此各有優點。
今天我們來研究一下redux-thunk的原始碼,看看它到底做了什麼事情。
使用場景
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
//註冊thunk到applyMiddleware
const createStoreWithMiddleware = applyMiddleware(
thunk
)(createStore);
const store = createStoreWithMiddleware(rootReducer);
//action方法
function increment() {
return {
type: INCREMENT_COUNTER
};
}
//執行一個非同步的dispatch
function incrementAsync() {
return dispatch => {
setTimeout(() => {
dispatch(increment());
}, 1000);
};
}複製程式碼
主要程式碼:
1、匯入thunk
import thunk from 'redux-thunk';複製程式碼
2、新增到applyMiddleware()
const createStoreWithMiddleware = applyMiddleware(
thunk
)(createStore);複製程式碼
我們可以猜測thunk是一個object。
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;複製程式碼
一共11行,簡潔,超簡潔,5K+ star。
原始碼分析
1、定義了createThunkMiddleware()方法,可以傳入引數extraArgument。
function createThunkMiddleware(extraArgument){}複製程式碼
2、該方法返回的是一個action物件。
我們知道action本身是一個object,帶有type和arguments。我們將dispatch和getState傳入action,next()和action()是redux提供的方法。接著做判斷,如果action是一個function,就返回action(dispatch, getState, extraArgument),否則返回next(action)。
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};複製程式碼
3、執行createThunkMiddleware()
這一步的常量thunk是一個物件,類似{type: "", arg1, arg2, ...}
const thunk = createThunkMiddleware();複製程式碼
4、給thunk設定一個變數withExtraArgument,並且將createThunkMiddleware整個函式賦給它。
thunk.withExtraArgument = createThunkMiddleware;複製程式碼
5、最後匯出thunk。
export default thunk;複製程式碼
總結
什麼是thunk?thunk是一箇中間函式,它的返回值是一個表示式。action裡面可能傳遞多個引數,我們不可能再專門替每個action寫一個傳遞方法。那麼就有了thunk的出現,thunk可以將多個引數的函式作為一個引數傳遞。
例如有這樣一個action,帶有多個引數:
function test(arg1, arg2, ...) {
return {
type: "TEST",
arg1,
arg2,
...
}
}複製程式碼
然後我們執行dispatch()方法,我們需要把test()函式作為一個引數傳遞。這樣就解決了多引數傳遞的問題,這個test()就成了一個thunk。
如果你對redux-thunk還有疑問,可以檢視這個解釋:redux-thunk of stackoverflow