Redux 在幾天前(2018.04.18)發(fā)布了新版本,6 commits 被合入 master。從誕生起朽们,到如今 4.0 版本,Redux 保持了使用層面的平滑過(guò)渡酥郭。同時(shí)前不久华坦, React 也從 15 升級(jí)到 16 版本愿吹,開發(fā)者并不需要作出太大的變動(dòng)不从,即可“無(wú)痛升級(jí)”。但是在版本迭代的背后很多有趣的設(shè)計(jì)值得了解犁跪。Redux 此次升級(jí)同樣如此椿息。
本文將從此次版本升級(jí)展開,從源代碼改動(dòng)入手坷衍,進(jìn)行分析寝优。通過(guò)后文內(nèi)容,相信讀者能夠在 JavaScript 基礎(chǔ)層面有更深認(rèn)識(shí)枫耳。
本文支持前端初學(xué)者學(xué)習(xí)乏矾,同時(shí)更適合有 Redux 源碼閱讀經(jīng)驗(yàn)者,核心源碼并不會(huì)重復(fù)分析迁杨,更多將聚焦在升級(jí)改動(dòng)上钻心。
改動(dòng)點(diǎn)總覽
這次升級(jí)改動(dòng)點(diǎn)一共有 22 處,最主要體現(xiàn)在 TypeScript 使用铅协、CommonJS 和 ES 構(gòu)建捷沸、關(guān)于 state 拋錯(cuò)三方面上。對(duì)于工程和配置的改動(dòng)狐史,我們不再多費(fèi)筆墨痒给。主要從代碼細(xì)節(jié)入手说墨,基礎(chǔ)入手,著重分析以下幾處改動(dòng):
- 中間件 API dispatch 參數(shù)處理苍柏;
- applyMiddleware 改動(dòng)尼斧;
- bindActionCreators 對(duì) this 透明化處理;
- dispatching 時(shí)试吁,對(duì) state 的凍結(jié)突颊;
- Plain Object 類型判斷;
話不多說(shuō)潘悼,我們直接進(jìn)入正題律秃。
applyMiddleware 參數(shù)處理
這項(xiàng)改動(dòng)由 Asvarox 提出。熟悉 Redux 源碼中 applyMiddleware.js 設(shè)計(jì)的讀者一定對(duì) middlewareAPI 并不陌生:對(duì)于每個(gè)中間件治唤,都可以感知部分 store棒动,即 middlewareAPI。這里簡(jiǎn)單展開一下:
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch)
創(chuàng)建一個(gè)中間件 store:
let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null);
我們看宾添,applyMiddleware 是個(gè)三級(jí) curry 化的函數(shù)船惨。它將陸續(xù)獲得了三個(gè)參數(shù),第一個(gè)是 middlewares 數(shù)組粱锐,[mid1, mid2, mid3, ...],第二個(gè)是 Redux 原生的 createStore扛邑,最后一個(gè)是 reducer怜浅;
applyMiddleware 利用 createStore 和 reducer 創(chuàng)建了一個(gè) store,然后 store 的 getState 方法和 dispatch 方法又分別被直接和間接地賦值給 middlewareAPI 變量蔬崩。middlewares 數(shù)組通過(guò) map 方法讓每個(gè) middleware 帶著 middlewareAPI 這個(gè)參數(shù)分別執(zhí)行一遍恶座。執(zhí)行完后,獲得 chain 數(shù)組沥阳,[f1, f2, ... , fx, ...,fn]跨琳,接著 compose 將 chain 中的所有匿名函數(shù),[f1, f2, ... , fx, ..., fn]桐罕,組裝成一個(gè)新的函數(shù)脉让,即新的 dispatch,當(dāng)新 dispatch 執(zhí)行時(shí)功炮,[f1, f2, ... , fx, ..., fn] 將會(huì)從右到左依次執(zhí)行溅潜。以上解釋改動(dòng)自:pure render 專欄。
好了死宣,把中間件機(jī)制簡(jiǎn)要解釋之后伟恶,我們看看這次改動(dòng)。故事源于 Asvarox 設(shè)計(jì)了一個(gè)自定義的中間件毅该,這個(gè)中間件接收的 dispatch 需要兩個(gè)參數(shù)博秫。他的“杰作”就像這樣:
const middleware = ({ dispatch }) => next => (actionCreator, args) => dispatch(actionCreator(...args));
對(duì)比傳統(tǒng)編寫中間件的套路:
const middleware = store => next => action => {...}
我們能清晰地看到他的這種編寫方式會(huì)有什么問(wèn)題:在原有 Redux 源碼基礎(chǔ)上潦牛,actionCreator 參數(shù)后面的 args 將會(huì)丟失。因此他提出的改動(dòng)點(diǎn)在:
const middlewareAPI = {
getState: store.getState,
- dispatch: (action) => dispatch(action)
+ dispatch: (...args) => dispatch(...args)
}
如果你好奇他為什么會(huì)這樣設(shè)計(jì)自己的中間件挡育,可以參考 #2501 號(hào) issue巴碗。我個(gè)人認(rèn)為對(duì)于需求來(lái)說(shuō),他的這種“奇葩”方式即寒,可以通過(guò)其他手段來(lái)規(guī)避橡淆;但是對(duì)于 Redux 庫(kù)來(lái)說(shuō),將 middlewareAPI.dispatch 參數(shù)展開母赵,確實(shí)是更合適的做法逸爵。
此項(xiàng)改動(dòng)我們點(diǎn)到為止,不再鉆牛角尖凹嘲。應(yīng)該學(xué)到:基于 ES6 的不定參數(shù)與展開運(yùn)算符的妙用师倔。雖然一直在說(shuō),一直在提周蹭,但在真正開發(fā)程序時(shí)趋艘,我們?nèi)匀灰獣r(shí)刻注意,并養(yǎng)成良好習(xí)慣凶朗。
基于此瓷胧,同樣的改動(dòng)也體現(xiàn)在:
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 = []
這項(xiàng)改動(dòng)由 jimbolla 提出。
bindActionCreators 對(duì) this 透明化處理
Redux 中的 bindActionCreators棚愤,達(dá)到 dispatch 將 action 包裹起來(lái)的目的搓萧。這樣通過(guò) bindActionCreators 創(chuàng)建的方法,可以直接調(diào)用 dispatch(action) (隱式調(diào)用)遇八∶妫可能很多開發(fā)者并不常用,所以這里稍微展開刃永,在 action.js 文件中, 我們定義了兩個(gè) 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>
}
}
這樣一來(lái)斯够,我們?cè)谧咏M件 Child 中,直接調(diào)用 newAction.action1 就相當(dāng)于調(diào)用 dispatch(action1)喧锦,如此做的好處在于:沒(méi)有 store 和 dispatch 的組件读规,也可以進(jìn)行動(dòng)作分發(fā)。
一般這個(gè) API 應(yīng)用不多燃少,至少筆者不太常用束亏。因此上面做一個(gè)簡(jiǎn)單介紹。有經(jīng)驗(yàn)的開發(fā)中一定不難猜出 bindActionCreators 源碼做了什么阵具,連帶著這次改動(dòng):
function bindActionCreator(actionCreator, dispatch) {
- return (...args) => dispatch(actionCreator(...args))
+ return function() { return dispatch(actionCreator.apply(this, arguments)) }
}
我們看這次改動(dòng)碍遍,對(duì) actionCreator 使用 apply 方法定铜,明確地進(jìn)行 this 綁定。那么這樣做的意義在哪里呢怕敬?
我舉一個(gè)例子揣炕,想象我們對(duì)原始的 actionCreator 進(jìn)行 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);
我們應(yīng)該期望 boundAction 和 action 一致东跪;且 boundAction.this 和 uniqueThis 一致畸陡,都等同于 action.this。這如此的期望下虽填,這樣的改動(dòng)無(wú)疑是必須的丁恭。
對(duì) state 的凍結(jié)
Dan Abramov 認(rèn)為,在 reducer 中使用 getState() 和 subscribe() 方法是一種反模式斋日。store.getState 的調(diào)用會(huì)使得 reducer 不純涩惑。事實(shí)上,原版已經(jīng)在 reducer 執(zhí)行過(guò)程中桑驱,禁用了 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
}
同時(shí),這次修改在 getState 方法以及 subscribe熬的、unsubscribe 方法中進(jìn)行了同樣的凍結(jié)處理:
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.'
)
}
筆者認(rèn)為痊硕,這樣的做法毫無(wú)爭(zhēng)議。顯式拋出異常無(wú)意是合理的押框。
Plain Object 類型判斷
Plain Object 是一個(gè)非常有趣的概念岔绸。這次改動(dòng)圍繞判斷 Plain Object 的性能進(jìn)行了激烈的討論。最終將引用 lodash isPlainObject 的判斷方法改為 ./utils/isPlainObject 中自己封裝的做法:
- import isPlainObject from 'lodash/isPlainObject';
+ import isPlainObject from './utils/isPlainObject'
簡(jiǎn)單來(lái)說(shuō)橡伞,Plain Object:
指的是通過(guò)字面量形式或者new Object()形式定義的對(duì)象盒揉。
Redux 這次使用了以下代碼來(lái)進(jìn)行判斷:
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
}
如果讀者對(duì)上述代碼不理解,那么需要補(bǔ)一下原型兑徘、原型鏈的知識(shí)刚盈。簡(jiǎn)單來(lái)說(shuō),就是判斷 obj 的原型鏈有幾層挂脑,只有一層就返回 true藕漱。如果還不理解,可以參考下面示例代碼:
function Foo() {}
// obj 不是一個(gè) 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 的實(shí)現(xiàn)為:
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
}
根據(jù) timdorr 給出的對(duì)比結(jié)果崭闲,dispatch 方法中:
master: 4690.358ms
nodash: 82.821ms
這一組 benchmark 引發(fā)的討論自然少不了肋联,也引出來(lái)了 Dan Abramov。筆者對(duì)此不發(fā)表任何意見(jiàn)刁俭,感興趣的同學(xué)可自行研究橄仍。從結(jié)果上來(lái)看,摒除了部分對(duì) lodash 的依賴,在性能表現(xiàn)上說(shuō)服力增強(qiáng)侮繁。
展望和總結(jié)
提到 Redux 發(fā)展虑粥,自然離不開 React,React 新版本一經(jīng)推出鼎天,極受追捧舀奶。尤其是 context 這樣的新 API,某些開發(fā)者認(rèn)為將逐漸取代 Redux斋射。
筆者認(rèn)為育勺,圍繞 React 開發(fā)應(yīng)用,數(shù)據(jù)狀態(tài)管理始終是一個(gè)極其重要的話題罗岖。但是 React context 和 Redux 并不是完全對(duì)立的涧至。
首先 React 新特性 context 在大型數(shù)據(jù)應(yīng)用的前提下,并不會(huì)減少模版代碼桑包。而其 Provider 和 Consumer 的一一對(duì)應(yīng)特性南蓬,即 Provider 和 Consumer 必須來(lái)自同一次 React.createContext 調(diào)用(可以用 hack 方式解決此“局限”),仿佛 React 團(tuán)隊(duì)對(duì)于此特性的發(fā)展方向設(shè)計(jì)主要體現(xiàn)在小型狀態(tài)管理上哑了。如果需要實(shí)現(xiàn)更加靈活和直接的操作赘方,Redux 也許會(huì)是更好的選擇。
其次弱左,Redux 豐富的生態(tài)以及中間件等機(jī)制窄陡,決定了其在很大程度上具有不可替代性。畢竟拆火,已經(jīng)使用 Redux 的項(xiàng)目跳夭,遷移成本也將是極大的,至少需要開發(fā)中先升級(jí) React 以支持新版 context 吧们镜。
最后币叹,Redux 作為一個(gè)“發(fā)布訂閱系統(tǒng)”,完全可以脫離 React 而單獨(dú)存在模狭,這樣的基因也決定了其后天與 React 本身 context 不同的性征颈抚。
我認(rèn)為,新版 React context 是對(duì) React 本身“短板”的長(zhǎng)線補(bǔ)充和完善胞皱,未來(lái)大概率也會(huì)有所打磨調(diào)整邪意。Redux 也會(huì)進(jìn)行一系列迭代,但就如同這次版本升級(jí)一樣反砌,將趨于穩(wěn)定,更多的是細(xì)節(jié)上調(diào)整萌朱。
退一步講宴树,React context 的確也和 Redux 有千絲萬(wàn)縷的聯(lián)系。任何類庫(kù)或者框架都具有其短板晶疼,Redux 同樣也如此酒贬。我們完全可以使用新版 React context又憨,在使用層面來(lái)規(guī)避 Redux 的一些劣勢(shì),模仿 Redux 所能做到的一切锭吨。如同 didierfranc 的 react-waterfall蠢莺,國(guó)內(nèi)@方正的 Rectx,都是基于新版 React context 的解決方案零如。
最后躏将,我很贊同@誠(chéng)身所說(shuō):
選擇用什么樣的工具從來(lái)都不是決定一個(gè)開發(fā)團(tuán)隊(duì)成敗的關(guān)鍵,根據(jù)業(yè)務(wù)場(chǎng)景選擇恰當(dāng)?shù)墓ぞ呖祭伲⒗霉ぞ叻催^(guò)來(lái)約束開發(fā)者祸憋,最終達(dá)到控制整體項(xiàng)目復(fù)雜度的目的,才是促進(jìn)一個(gè)開發(fā)團(tuán)隊(duì)不斷提升的核心動(dòng)力肖卧。
沒(méi)錯(cuò)蚯窥,真正對(duì)項(xiàng)目起到?jīng)Q定性作用的還是是開發(fā)者本身,完善基礎(chǔ)知識(shí)塞帐,提升開發(fā)技能拦赠,讓我們從 Redux 4.0 的改動(dòng)看起吧。
廣告時(shí)間:
如果你對(duì)前端發(fā)展葵姥,尤其對(duì) React 技術(shù)棧感興趣:我的新書中荷鼠,也許有你想看到的內(nèi)容。關(guān)注作者 Lucas HC牌里,新書出版將會(huì)有送書活動(dòng)颊咬。
Happy Coding!
PS: 作者 Github倉(cāng)庫(kù) 和 知乎問(wèn)答鏈接 歡迎各種形式交流!
我的其他幾篇關(guān)于React技術(shù)棧的文章:
從setState promise化的探討 體會(huì)React團(tuán)隊(duì)設(shè)計(jì)思想
React 應(yīng)用設(shè)計(jì)之道 - curry 化妙用
組件復(fù)用那些事兒 - React 實(shí)現(xiàn)按需加載輪子
通過(guò)實(shí)例牡辽,學(xué)習(xí)編寫 React 組件的“最佳實(shí)踐”
從 React 綁定 this喳篇,看 JS 語(yǔ)言發(fā)展和框架設(shè)計(jì)
做出Uber移動(dòng)網(wǎng)頁(yè)版還不夠 極致性能打造才見(jiàn)真章**
React+Redux打造“NEWS EARLY”單頁(yè)應(yīng)用 一個(gè)項(xiàng)目理解最前沿技術(shù)棧真諦