摘要:在幾天前發(fā)布了新版本,被合入。但是在版本迭代的背后很多有趣的設計值得了解。參數(shù)處理這項改動由提出。對透明化處理中的,達到將包裹起來的目的。對的凍結(jié)認為,在中使用和方法是一種反模式。尤其是這樣的新,某些開發(fā)者認為將逐漸取代。
Redux 在幾天前(2018.04.18)發(fā)布了新版本,6 commits 被合入 master。從誕生起,到如今 4.0 版本,Redux 保持了使用層面的平滑過渡。同時前不久, React 也從 15 升級到 16 版本,開發(fā)者并不需要作出太大的變動,即可“無痛升級”。但是在版本迭代的背后很多有趣的設計值得了解。Redux 此次升級同樣如此。
本文將從此次版本升級展開,從源代碼改動入手,進行分析。通過后文內(nèi)容,相信讀者能夠在 JavaScript 基礎層面有更深認識。
本文支持前端初學者學習,同時更適合有 Redux 源碼閱讀經(jīng)驗者,核心源碼并不會重復分析,更多將聚焦在升級改動上。
改動點總覽這次升級改動點一共有 22 處,最主要體現(xiàn)在 TypeScript 使用、CommonJS 和 ES 構(gòu)建、關于 state 拋錯三方面上。對于工程和配置的改動,我們不再多費筆墨。主要從代碼細節(jié)入手,基礎入手,著重分析以下幾處改動:
中間件 API dispatch 參數(shù)處理;
applyMiddleware 改動;
bindActionCreators 對 this 透明化處理;
dispatching 時,對 state 的凍結(jié);
Plain Object 類型判斷;
話不多說,我們直接進入正題。
applyMiddleware 參數(shù)處理這項改動由 Asvarox 提出。熟悉 Redux 源碼中 applyMiddleware.js 設計的讀者一定對 middlewareAPI 并不陌生:對于每個中間件,都可以感知部分 store,即 middlewareAPI。這里簡單展開一下:
const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) }; chain = middlewares.map(middleware => middleware(middlewareAPI)); dispatch = compose(...chain)(store.dispatch)
創(chuàng)建一個中間件 store:
let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null);
我們看,applyMiddleware 是個三級 curry 化的函數(shù)。它將陸續(xù)獲得了三個參數(shù),第一個是 middlewares 數(shù)組,[mid1, mid2, mid3, ...],第二個是 Redux 原生的 createStore,最后一個是 reducer;
applyMiddleware 利用 createStore 和 reducer 創(chuàng)建了一個 store,然后 store 的 getState 方法和 dispatch 方法又分別被直接和間接地賦值給 middlewareAPI 變量。middlewares 數(shù)組通過 map 方法讓每個 middleware 帶著 middlewareAPI 這個參數(shù)分別執(zhí)行一遍。執(zhí)行完后,獲得 chain 數(shù)組,[f1, f2, ... , fx, ...,fn],接著 compose 將 chain 中的所有匿名函數(shù),[f1, f2, ... , fx, ..., fn],組裝成一個新的函數(shù),即新的 dispatch,當新 dispatch 執(zhí)行時,[f1, f2, ... , fx, ..., fn] 將會從右到左依次執(zhí)行。以上解釋改動自:pure render 專欄。
好了,把中間件機制簡要解釋之后,我們看看這次改動。故事源于 Asvarox 設計了一個自定義的中間件,這個中間件接收的 dispatch 需要兩個參數(shù)。他的“杰作”就像這樣:
const middleware = ({ dispatch }) => next => (actionCreator, args) => dispatch(actionCreator(...args));
對比傳統(tǒng)編寫中間件的套路:
const middleware = store => next => action => {...}
我們能清晰地看到他的這種編寫方式會有什么問題:在原有 Redux 源碼基礎上,actionCreator 參數(shù)后面的 args 將會丟失。因此他提出的改動點在:
const middlewareAPI = { getState: store.getState, - dispatch: (action) => dispatch(action) + dispatch: (...args) => dispatch(...args) }
如果你好奇他為什么會這樣設計自己的中間件,可以參考 #2501 號 issue。我個人認為對于需求來說,他的這種“奇葩”方式,可以通過其他手段來規(guī)避;但是對于 Redux 庫來說,將 middlewareAPI.dispatch 參數(shù)展開,確實是更合適的做法。
此項改動我們點到為止,不再鉆牛角尖。應該學到:基于 ES6 的不定參數(shù)與展開運算符的妙用。雖然一直在說,一直在提,但在真正開發(fā)程序時,我們?nèi)匀灰獣r刻注意,并養(yǎ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 = []
這項改動由 jimbolla 提出。
bindActionCreators 對 this 透明化處理Redux 中的 bindActionCreators,達到 dispatch 將 action 包裹起來的目的。這樣通過 bindActionCreators 創(chuàng)建的方法,可以直接調(diào)用 dispatch(action) (隱式調(diào)用)??赡芎芏嚅_發(fā)者并不常用,所以這里稍微展開,在 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 中,直接調(diào)用 newAction.action1 就相當于調(diào)用 dispatch(action1),如此做的好處在于:沒有 store 和 dispatch 的組件,也可以進行動作分發(fā)。
一般這個 API 應用不多,至少筆者不太常用。因此上面做一個簡單介紹。有經(jīng)驗的開發(fā)中一定不難猜出 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。這如此的期望下,這樣的改動無疑是必須的。
對 state 的凍結(jié)Dan Abramov 認為,在 reducer 中使用 getState() 和 subscribe() 方法是一種反模式。store.getState 的調(diào)用會使得 reducer 不純。事實上,原版已經(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 }
同時,這次修改在 getState 方法以及 subscribe、unsubscribe 方法中進行了同樣的凍結(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." ) }
筆者認為,這樣的做法毫無爭議。顯式拋出異常無意是合理的。
Plain Object 類型判斷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 的實現(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 給出的對比結(jié)果,dispatch 方法中:
master: 4690.358ms nodash: 82.821ms
這一組 benchmark 引發(fā)的討論自然少不了,也引出來了 Dan Abramov。筆者對此不發(fā)表任何意見,感興趣的同學可自行研究。從結(jié)果上來看,摒除了部分對 lodash 的依賴,在性能表現(xiàn)上說服力增強。
展望和總結(jié)提到 Redux 發(fā)展,自然離不開 React,React 新版本一經(jīng)推出,極受追捧。尤其是 context 這樣的新 API,某些開發(fā)者認為將逐漸取代 Redux。
筆者認為,圍繞 React 開發(fā)應用,數(shù)據(jù)狀態(tài)管理始終是一個極其重要的話題。但是 React context 和 Redux 并不是完全對立的。
首先 React 新特性 context 在大型數(shù)據(jù)應用的前提下,并不會減少模版代碼。而其 Provider 和 Consumer 的一一對應特性,即 Provider 和 Consumer 必須來自同一次 React.createContext 調(diào)用(可以用 hack 方式解決此“局限”),仿佛 React 團隊對于此特性的發(fā)展方向設計主要體現(xiàn)在小型狀態(tài)管理上。如果需要實現(xiàn)更加靈活和直接的操作,Redux 也許會是更好的選擇。
其次,Redux 豐富的生態(tài)以及中間件等機制,決定了其在很大程度上具有不可替代性。畢竟,已經(jīng)使用 Redux 的項目,遷移成本也將是極大的,至少需要開發(fā)中先升級 React 以支持新版 context 吧。
最后,Redux 作為一個“發(fā)布訂閱系統(tǒng)”,完全可以脫離 React 而多帶帶存在,這樣的基因也決定了其后天與 React 本身 context 不同的性征。
我認為,新版 React context 是對 React 本身“短板”的長線補充和完善,未來大概率也會有所打磨調(diào)整。Redux 也會進行一系列迭代,但就如同這次版本升級一樣,將趨于穩(wěn)定,更多的是細節(jié)上調(diào)整。
退一步講,React context 的確也和 Redux 有千絲萬縷的聯(lián)系。任何類庫或者框架都具有其短板,Redux 同樣也如此。我們完全可以使用新版 React context,在使用層面來規(guī)避 Redux 的一些劣勢,模仿 Redux 所能做到的一切。如同 didierfranc 的 react-waterfall,國內(nèi)@方正的 Rectx,都是基于新版 React context 的解決方案。
最后,我很贊同@誠身所說:
選擇用什么樣的工具從來都不是決定一個開發(fā)團隊成敗的關鍵,根據(jù)業(yè)務場景選擇恰當?shù)墓ぞ?,并利用工具反過來約束開發(fā)者,最終達到控制整體項目復雜度的目的,才是促進一個開發(fā)團隊不斷提升的核心動力。
沒錯,真正對項目起到?jīng)Q定性作用的還是是開發(fā)者本身,完善基礎知識,提升開發(fā)技能,讓我們從 Redux 4.0 的改動看起吧。
廣告時間:
如果你對前端發(fā)展,尤其對 React 技術(shù)棧感興趣:我的新書中,也許有你想看到的內(nèi)容。關注作者 Lucas HC,新書出版將會有送書活動。
Happy Coding!
PS: 作者?Github倉庫?和?知乎問答鏈接?歡迎各種形式交流!
我的其他幾篇關于React技術(shù)棧的文章:
從setState promise化的探討 體會React團隊設計思想
React 應用設計之道 - curry 化妙用
組件復用那些事兒 - React 實現(xiàn)按需加載輪子
通過實例,學習編寫 React 組件的“最佳實踐”
React 組件設計和分解思考
從 React 綁定 this,看 JS 語言發(fā)展和框架設計
做出Uber移動網(wǎng)頁版還不夠 極致性能打造才見真章**
React+Redux打造“NEWS EARLY”單頁應用 一個項目理解最前沿技術(shù)棧真諦
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/94562.html
摘要:在幾天前發(fā)布了新版本,被合入。但是在版本迭代的背后很多有趣的設計值得了解。參數(shù)處理這項改動由提出。對透明化處理中的,達到將包裹起來的目的。對的凍結(jié)認為,在中使用和方法是一種反模式。尤其是這樣的新,某些開發(fā)者認為將逐漸取代。 showImg(https://segmentfault.com/img/remote/1460000014571148); Redux 在幾天前(2018.04....
摘要:技術(shù)前端布局推進劑間距規(guī)范化利用變量實現(xiàn)令人震驚的懸浮效果很棒,但有些情況不適用布局說可能是最全的圖片版學習網(wǎng)格布局使用的九大誤區(qū)圖解布局布局揭秘和中新增功能探索版本迭代論基礎談展望對比探究繪圖中撤銷功能的實現(xiàn)方式即將更改的生命周期幾道高 技術(shù) CSS 前端布局推進劑 - 間距規(guī)范化 利用CSS變量實現(xiàn)令人震驚的懸浮效果 Flexbox 很棒,但有些情況不適用 CSS布局說——可能是最...
摘要:精讀前端可以從多個角度理解,比如規(guī)范框架語言社區(qū)場景以及整條研發(fā)鏈路。同是前端未來展望,不同的文章側(cè)重的格局不同,兩個標題相同的文章內(nèi)容可能大相徑庭。作為使用者,現(xiàn)在和未來的主流可能都是微軟系,畢竟微軟在操作系統(tǒng)方面人才儲備和經(jīng)驗積累很多。 1. 引言 前端展望的文章越來越不好寫了,隨著前端發(fā)展的深入,需要擁有非常寬廣的視野與格局才能看清前端的未來。 筆者根據(jù)自身經(jīng)驗,結(jié)合下面幾篇文章...
閱讀 3509·2021-11-19 09:40
閱讀 1528·2021-10-13 09:41
閱讀 2709·2021-09-29 09:35
閱讀 2751·2021-09-23 11:21
閱讀 1748·2021-09-09 11:56
閱讀 860·2019-08-30 15:53
閱讀 862·2019-08-30 15:52
閱讀 623·2019-08-30 12:47