摘要:在幾天前發(fā)布了新版本,被合入。但是在版本迭代的背后很多有趣的設(shè)計(jì)值得了解。參數(shù)處理這項(xiàng)改動(dòng)由提出。對(duì)透明化處理中的,達(dá)到將包裹起來的目的。對(duì)的凍結(jié)認(rèn)為,在中使用和方法是一種反模式。尤其是這樣的新,某些開發(fā)者認(rèn)為將逐漸取代。
Redux 在幾天前(2018.04.18)發(fā)布了新版本,6 commits 被合入 master。從誕生起,到如今 4.0 版本,Redux 保持了使用層面的平滑過渡。同時(shí)前不久, React 也從 15 升級(jí)到 16 版本,開發(fā)者并不需要作出太大的變動(dòng),即可“無痛升級(jí)”。但是在版本迭代的背后很多有趣的設(shè)計(jì)值得了解。Redux 此次升級(jí)同樣如此。
本文將從此次版本升級(jí)展開,從源代碼改動(dòng)入手,進(jìn)行分析。通過后文內(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 類型判斷;
話不多說,我們直接進(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ù)組通過 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ì)有什么問題:在原有 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ì)于需求來說,他的這種“奇葩”方式,可以通過其他手段來規(guī)避;但是對(duì)于 Redux 庫來說,將 middlewareAPI.dispatch 參數(shù)展開,確實(shí)是更合適的做法。
此項(xiàng)改動(dòng)我們點(diǎn)到為止,不再鉆牛角尖。應(yīng)該學(xué)到:基于 ES6 的不定參數(shù)與展開運(yùn)算符的妙用。雖然一直在說,一直在提,但在真正開發(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 包裹起來的目的。這樣通過 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} }
這樣一來,我們?cè)谧咏M件 Child 中,直接調(diào)用 newAction.action1 就相當(dāng)于調(diào)用 dispatch(action1),如此做的好處在于:沒有 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)無疑是必須的。
對(duì) state 的凍結(jié)Dan Abramov 認(rèn)為,在 reducer 中使用 getState() 和 subscribe() 方法是一種反模式。store.getState 的調(diào)用會(huì)使得 reducer 不純。事實(shí)上,原版已經(jīng)在 reducer 執(zhí)行過程中,禁用了 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)為,這樣的做法毫無爭(zhēng)議。顯式拋出異常無意是合理的。
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)單來說,Plain Object:
指的是通過字面量形式或者new Object()形式定義的對(duì)象。
Redux 這次使用了以下代碼來進(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)單來說,就是判斷 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ā)的討論自然少不了,也引出來了 Dan Abramov。筆者對(duì)此不發(fā)表任何意見,感興趣的同學(xué)可自行研究。從結(jié)果上來看,摒除了部分對(duì) lodash 的依賴,在性能表現(xiàn)上說服力增強(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 必須來自同一次 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 而多帶帶存在,這樣的基因也決定了其后天與 React 本身 context 不同的性征。
我認(rèn)為,新版 React context 是對(duì) React 本身“短板”的長線補(bǔ)充和完善,未來大概率也會(huì)有所打磨調(diào)整。Redux 也會(huì)進(jìn)行一系列迭代,但就如同這次版本升級(jí)一樣,將趨于穩(wěn)定,更多的是細(xì)節(jié)上調(diào)整。
退一步講,React context 的確也和 Redux 有千絲萬縷的聯(lián)系。任何類庫或者框架都具有其短板,Redux 同樣也如此。我們完全可以使用新版 React context,在使用層面來規(guī)避 Redux 的一些劣勢(shì),模仿 Redux 所能做到的一切。如同 didierfranc 的 react-waterfall,國內(nèi)@方正的 Rectx,都是基于新版 React context 的解決方案。
最后,我很贊同@誠身所說:
選擇用什么樣的工具從來都不是決定一個(gè)開發(fā)團(tuán)隊(duì)成敗的關(guān)鍵,根據(jù)業(yè)務(wù)場(chǎng)景選擇恰當(dāng)?shù)墓ぞ撸⒗霉ぞ叻催^來約束開發(fā)者,最終達(dá)到控制整體項(xiàng)目復(fù)雜度的目的,才是促進(jìn)一個(gè)開發(fā)團(tuán)隊(duì)不斷提升的核心動(dòng)力。
沒錯(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倉庫?和?知乎問答鏈接?歡迎各種形式交流!
我的其他幾篇關(guān)于React技術(shù)棧的文章:
從setState promise化的探討 體會(huì)React團(tuán)隊(duì)設(shè)計(jì)思想
React 應(yīng)用設(shè)計(jì)之道 - curry 化妙用
組件復(fù)用那些事兒 - React 實(shí)現(xiàn)按需加載輪子
通過實(shí)例,學(xué)習(xí)編寫 React 組件的“最佳實(shí)踐”
React 組件設(shè)計(jì)和分解思考
從 React 綁定 this,看 JS 語言發(fā)展和框架設(shè)計(jì)
做出Uber移動(dòng)網(wǎng)頁版還不夠 極致性能打造才見真章**
React+Redux打造“NEWS EARLY”單頁應(yīng)用 一個(gè)項(xiàng)目理解最前沿技術(shù)棧真諦
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/52247.html
摘要:在幾天前發(fā)布了新版本,被合入。但是在版本迭代的背后很多有趣的設(shè)計(jì)值得了解。參數(shù)處理這項(xiàng)改動(dòng)由提出。對(duì)透明化處理中的,達(dá)到將包裹起來的目的。對(duì)的凍結(jié)認(rèn)為,在中使用和方法是一種反模式。尤其是這樣的新,某些開發(fā)者認(rèn)為將逐漸取代。 showImg(https://segmentfault.com/img/remote/1460000014571148); Redux 在幾天前(2018.04....
摘要:技術(shù)前端布局推進(jìn)劑間距規(guī)范化利用變量實(shí)現(xiàn)令人震驚的懸浮效果很棒,但有些情況不適用布局說可能是最全的圖片版學(xué)習(xí)網(wǎng)格布局使用的九大誤區(qū)圖解布局布局揭秘和中新增功能探索版本迭代論基礎(chǔ)談?wù)雇麑?duì)比探究繪圖中撤銷功能的實(shí)現(xiàn)方式即將更改的生命周期幾道高 技術(shù) CSS 前端布局推進(jìn)劑 - 間距規(guī)范化 利用CSS變量實(shí)現(xiàn)令人震驚的懸浮效果 Flexbox 很棒,但有些情況不適用 CSS布局說——可能是最...
摘要:技術(shù)前端布局推進(jìn)劑間距規(guī)范化利用變量實(shí)現(xiàn)令人震驚的懸浮效果很棒,但有些情況不適用布局說可能是最全的圖片版學(xué)習(xí)網(wǎng)格布局使用的九大誤區(qū)圖解布局布局揭秘和中新增功能探索版本迭代論基礎(chǔ)談?wù)雇麑?duì)比探究繪圖中撤銷功能的實(shí)現(xiàn)方式即將更改的生命周期幾道高 技術(shù) CSS 前端布局推進(jìn)劑 - 間距規(guī)范化 利用CSS變量實(shí)現(xiàn)令人震驚的懸浮效果 Flexbox 很棒,但有些情況不適用 CSS布局說——可能是最...
摘要:精讀前端可以從多個(gè)角度理解,比如規(guī)范框架語言社區(qū)場(chǎng)景以及整條研發(fā)鏈路。同是前端未來展望,不同的文章側(cè)重的格局不同,兩個(gè)標(biāo)題相同的文章內(nèi)容可能大相徑庭。作為使用者,現(xiàn)在和未來的主流可能都是微軟系,畢竟微軟在操作系統(tǒng)方面人才儲(chǔ)備和經(jīng)驗(yàn)積累很多。 1. 引言 前端展望的文章越來越不好寫了,隨著前端發(fā)展的深入,需要擁有非常寬廣的視野與格局才能看清前端的未來。 筆者根據(jù)自身經(jīng)驗(yàn),結(jié)合下面幾篇文章...
閱讀 2627·2021-11-17 17:00
閱讀 1876·2021-10-11 10:57
閱讀 3748·2021-09-09 11:33
閱讀 917·2021-09-09 09:33
閱讀 3555·2019-08-30 14:20
閱讀 3321·2019-08-29 11:25
閱讀 2803·2019-08-26 13:48
閱讀 743·2019-08-26 11:52