Redux 在幾天前(2018.04.18)發佈了新版本,6 commits 被合入 master。從誕生起,到現在 4.0 版本,Redux 保持了使用層面的平滑過渡。同時前不久, React 也從 15 升級到 16 版本,開發者並不須要做出太大的變更,便可「無痛升級」。可是在版本迭代的背後不少有趣的設計值得了解。Redux 這次升級一樣如此。前端
本文將今後次版本升級展開,從源代碼改動入手,進行分析。經過後文內容,相信讀者可以在 JavaScript 基礎層面有更深認識。react
本文支持前端初學者學習,同時更適合有 Redux 源碼閱讀經驗者,核心源碼並不會重複分析,更多將聚焦在升級改動上。git
此次升級改動點一共有 22 處,最主要體如今 TypeScript 使用、CommonJS 和 ES 構建、關於 state 拋錯三方面上。對於工程和配置的改動,咱們再也不多費筆墨。主要從代碼細節入手,基礎入手,着重分析如下幾處改動:github
話很少說,咱們直接進入正題。redux
這項改動由 Asvarox 提出。熟悉 Redux 源碼中 applyMiddleware.js 設計的讀者必定對 middlewareAPI 並不陌生:對於每一箇中間件,均可以感知部分 store,即 middlewareAPI。這裏簡單展開一下:api
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch)
複製代碼
建立一箇中間件 store:數組
let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null);
複製代碼
咱們看,applyMiddleware 是個三級 curry 化的函數。它將陸續得到了三個參數,第一個是 middlewares 數組,[mid1, mid2, mid3, ...],第二個是 Redux 原生的 createStore,最後一個是 reducer;promise
applyMiddleware 利用 createStore 和 reducer 建立了一個 store,而後 store 的 getState 方法和 dispatch 方法又分別被直接和間接地賦值給 middlewareAPI 變量。middlewares 數組經過 map 方法讓每一個 middleware 帶着 middlewareAPI 這個參數分別執行一遍。執行完後,得到 chain 數組,[f1, f2, ... , fx, ...,fn],接着 compose 將 chain 中的全部匿名函數,[f1, f2, ... , fx, ..., fn],組裝成一個新的函數,即新的 dispatch,當新 dispatch 執行時,[f1, f2, ... , fx, ..., fn] 將會從右到左依次執行。以上解釋改動自:pure render 專欄。app
好了,把中間件機制簡要解釋以後,咱們看看此次改動。故事源於 Asvarox 設計了一個自定義的中間件,這個中間件接收的 dispatch 須要兩個參數。他的「傑做」就像這樣:框架
const middleware = ({ dispatch }) => next => (actionCreator, args) => dispatch(actionCreator(...args));
複製代碼
對比傳統編寫中間件的套路:
const middleware = store => next => action => {...}
複製代碼
咱們能清晰地看到他的這種編寫方式會有什麼問題:在原有 Redux 源碼基礎上,actionCreator 參數後面的 args 將會丟失。所以他提出的改動點在:
const middlewareAPI = {
getState: store.getState,
- dispatch: (action) => dispatch(action)
+ dispatch: (...args) => dispatch(...args)
}
複製代碼
若是你好奇他爲何會這樣設計本身的中間件,能夠參考 #2501 號 issue。我我的認爲對於需求來講,他的這種「奇葩」方式,能夠經過其餘手段來規避;可是對於 Redux 庫來講,將 middlewareAPI.dispatch 參數展開,確實是更合適的作法。
此項改動咱們點到爲止,再也不鑽牛角尖。應該學到:基於 ES6 的不定參數與展開運算符的妙用。雖然一直在說,一直在提,但在真正開發程序時,咱們仍然要時刻注意,並養成良好習慣。
基於此,一樣的改動也體如今:
export default function applyMiddleware(...middlewares) {
- return (createStore) => (reducer, preloadedState, enhancer) => {
- const store = createStore(reducer, preloadedState, enhancer)
+ return (createStore) => (...args) => {
+ const store = createStore(...args)
let dispatch = store.dispatch
let chain = []
複製代碼
這項改動由 jimbolla 提出。
Redux 中的 bindActionCreators,達到 dispatch 將 action 包裹起來的目的。這樣經過 bindActionCreators 建立的方法,能夠直接調用 dispatch(action) (隱式調用)。可能不少開發者並不經常使用,因此這裏稍微展開,在 action.js 文件中, 咱們定義了兩個 action creators:
function action1(){
return {
type:'type1'
}
}
function action2(){
return {
type:'type2'
}
}
複製代碼
在另外一文件 SomeComponent.js 中,咱們即可以直接使用:
import { bindActionCreators } from 'redux';
import * as oldActionCreator from './action.js'
class C1 extends Component {
constructor(props) {
super(props);
const {dispatch} = props;
this.boundActionCreators = bindActionCreators(oldActionCreator, dispatch);
}
componentDidMount() {
// 由 react-redux 注入的 dispatch:
let { dispatch } = this.props;
let action = TodoActionCreators.addTodo('Use Redux');
dispatch(action);
}
render() {
// ...
let { dispatch } = this.props;
let newAction = bindActionCreators(oldActionCreator, dispatch)
return <Child {...newAction}></child>
}
}
複製代碼
這樣一來,咱們在子組件 Child 中,直接調用 newAction.action1 就至關於調用 dispatch(action1),如此作的好處在於:沒有 store 和 dispatch 的組件,也能夠進行動做分發。
通常這個 API 應用很少,至少筆者不太經常使用。所以上面作一個簡單介紹。有經驗的開發中必定不難猜出 bindActionCreators 源碼作了什麼,連帶着此次改動:
function bindActionCreator(actionCreator, dispatch) {
- return (...args) => dispatch(actionCreator(...args))
+ return function() { return dispatch(actionCreator.apply(this, arguments)) }
}
複製代碼
咱們看此次改動,對 actionCreator 使用 apply 方法,明確地進行 this 綁定。那麼這樣作的意義在哪裏呢?
我舉一個例子,想象咱們對原始的 actionCreator 進行 this 綁定,並使用 bindActionCreators 方法:
const uniqueThis = {};
function actionCreator() {
return { type: 'UNKNOWN_ACTION', this: this, args: [...arguments] }
};
const action = actionCreator.apply(uniqueThis,argArray);
const boundActionCreator = bindActionCreators(actionCreator, store.dispatch);
const boundAction = boundActionCreator.apply(uniqueThis,argArray);
複製代碼
咱們應該指望 boundAction 和 action 一致;且 boundAction.this 和 uniqueThis 一致,都等同於 action.this。這如此的指望下,這樣的改動無疑是必須的。
Dan Abramov 認爲,在 reducer 中使用 getState() 和 subscribe() 方法是一種反模式。store.getState 的調用會使得 reducer 不純。事實上,原版已經在 reducer 執行過程當中,禁用了 dispatch 方法。源碼以下:
function dispatch(action) {
// ...
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
var listeners = currentListeners = nextListeners
for (var i = 0; i < listeners.length; i++) {
listeners[i]()
}
return action
}
複製代碼
同時,此次修改在 getState 方法以及 subscribe、unsubscribe 方法中進行了一樣的凍結處理:
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
)
}
複製代碼
筆者認爲,這樣的作法毫無爭議。顯式拋出異常無心是合理的。
Plain Object 是一個很是有趣的概念。此次改動圍繞判斷 Plain Object 的性能進行了激烈的討論。最終將引用 lodash isPlainObject 的判斷方法改成 ./utils/isPlainObject 中本身封裝的作法:
- import isPlainObject from 'lodash/isPlainObject';
+ import isPlainObject from './utils/isPlainObject'
複製代碼
簡單來講,Plain Object:
指的是經過字面量形式或者new Object()形式定義的對象。
Redux 此次使用瞭如下代碼來進行判斷:
export default function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false
let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto
}
複製代碼
若是讀者對上述代碼不理解,那麼須要補一下原型、原型鏈的知識。簡單來講,就是判斷 obj 的原型鏈有幾層,只有一層就返回 true。若是還不理解,能夠參考下面示例代碼:
function Foo() {}
// obj 不是一個 plain object
var obj = new Foo();
console.log(typeof obj, obj !== null);
let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
// false
var isPlain = Object.getPrototypeOf(obj) === proto;
console.log(isPlain);
複製代碼
而 loadash 的實現爲:
function isPlainObject(value) {
if (!isObjectLike(value) || baseGetTag(value) != '[object Object]') {
return false
}
if (Object.getPrototypeOf(value) === null) {
return true
}
let proto = value
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(value) === proto
}
export default isPlainObject
複製代碼
isObjectLike 源碼:
function isObjectLike(value) {
return typeof value == 'object' && value !== null
}
複製代碼
baseGetTag 源碼:
const objectProto = Object.prototype
const hasOwnProperty = objectProto.hasOwnProperty
const toString = objectProto.toString
const symToStringTag = typeof Symbol != 'undefined' ? Symbol.toStringTag : undefined
function baseGetTag(value) {
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]'
}
if (!(symToStringTag && symToStringTag in Object(value))) {
return toString.call(value)
}
const isOwn = hasOwnProperty.call(value, symToStringTag)
const tag = value[symToStringTag]
let unmasked = false
try {
value[symToStringTag] = undefined
unmasked = true
} catch (e) {}
const result = toString.call(value)
if (unmasked) {
if (isOwn) {
value[symToStringTag] = tag
} else {
delete value[symToStringTag]
}
}
return result
}
複製代碼
根據 timdorr 給出的對比結果,dispatch 方法中:
master: 4690.358ms
nodash: 82.821ms
複製代碼
這一組 benchmark 引起的討論天然少不了,也引出來了 Dan Abramov。筆者對此不發表任何意見,感興趣的同窗可自行研究。從結果上來看,摒除了部分對 lodash 的依賴,在性能表現上說服力加強。
提到 Redux 發展,天然離不開 React,React 新版本一經推出,極受追捧。尤爲是 context 這樣的新 API,某些開發者認爲將逐漸取代 Redux。
筆者認爲,圍繞 React 開發應用,數據狀態管理始終是一個極其重要的話題。可是 React context 和 Redux 並非徹底對立的。
首先 React 新特性 context 在大型數據應用的前提下,並不會減小模版代碼。而其 Provider 和 Consumer 的一一對應特性,即 Provider 和 Consumer 必須來自同一次 React.createContext 調用(能夠用 hack 方式解決此「侷限」),彷彿 React 團隊對於此特性的發展方向設計主要體如今小型狀態管理上。若是須要實現更加靈活和直接的操做,Redux 也許會是更好的選擇。
其次,Redux 豐富的生態以及中間件等機制,決定了其在很大程度上具備不可替代性。畢竟,已經使用 Redux 的項目,遷移成本也將是極大的,至少須要開發中先升級 React 以支持新版 context 吧。
最後,Redux 做爲一個「發佈訂閱系統」,徹底能夠脫離 React 而單獨存在,這樣的基因也決定了其後天與 React 自己 context 不一樣的性徵。
我認爲,新版 React context 是對 React 自己「短板」的長線補充和完善,將來大機率也會有所打磨調整。Redux 也會進行一系列迭代,但就如同此次版本升級同樣,將趨於穩定,更多的是細節上調整。
退一步講,React context 的確也和 Redux 有千絲萬縷的聯繫。任何類庫或者框架都具備其短板,Redux 一樣也如此。咱們徹底可使用新版 React context,在使用層面來規避 Redux 的一些劣勢,模仿 Redux 所能作到的一切。如同 didierfranc 的 react-waterfall,國內@方正的 Rectx,都是基於新版 React context 的解決方案。
最後,我很贊同@誠身所說: 選擇用什麼樣的工具歷來都不是決定一個開發團隊成敗的關鍵,根據業務場景選擇恰當的工具,並利用工具反過來約束開發者,最終達到控制總體項目複雜度的目的,纔是促進一個開發團隊不斷提高的核心動力。
沒錯,真正對項目起到決定性做用的仍是是開發者自己,完善基礎知識,提高開發技能,讓咱們從 Redux 4.0 的改動看起吧。
廣告時間: 若是你對前端發展,尤爲對 React 技術棧感興趣:個人新書中,也許有你想看到的內容。關注做者 Lucas HC,新書出版將會有送書活動。
Happy Coding!
PS: 做者 Github倉庫 和 知乎問答連接 歡迎各類形式交流!
個人其餘幾篇關於React技術棧的文章:
從setState promise化的探討 體會React團隊設計思想