摘要:在幾天前發布了新版本,被合入。但是在版本迭代的背后很多有趣的設計值得了解。參數處理這項改動由提出。對透明化處理中的,達到將包裹起來的目的。對的凍結認為,在中使用和方法是一種反模式。尤其是這樣的新,某些開發者認為將逐漸取代。
Redux 在幾天前(2018.04.18)發布了新版本,6 commits 被合入 master。從誕生起,到如今 4.0 版本,Redux 保持了使用層面的平滑過渡。同時前不久, React 也從 15 升級到 16 版本,開發者并不需要作出太大的變動,即可“無痛升級”。但是在版本迭代的背后很多有趣的設計值得了解。Redux 此次升級同樣如此。
本文將從此次版本升級展開,從源代碼改動入手,進行分析。通過后文內容,相信讀者能夠在 JavaScript 基礎層面有更深認識。
本文支持前端初學者學習,同時更適合有 Redux 源碼閱讀經驗者,核心源碼并不會重復分析,更多將聚焦在升級改動上。
改動點總覽這次升級改動點一共有 22 處,最主要體現在 TypeScript 使用、CommonJS 和 ES 構建、關于 state 拋錯三方面上。對于工程和配置的改動,我們不再多費筆墨。主要從代碼細節入手,基礎入手,著重分析以下幾處改動:
中間件 API dispatch 參數處理;
applyMiddleware 改動;
bindActionCreators 對 this 透明化處理;
dispatching 時,對 state 的凍結;
Plain Object 類型判斷;
話不多說,我們直接進入正題。
applyMiddleware 參數處理這項改動由 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)
創建一個中間件 store:
let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null);
我們看,applyMiddleware 是個三級 curry 化的函數。它將陸續獲得了三個參數,第一個是 middlewares 數組,[mid1, mid2, mid3, ...],第二個是 Redux 原生的 createStore,最后一個是 reducer;
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 專欄。
好了,把中間件機制簡要解釋之后,我們看看這次改動。故事源于 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 提出。
bindActionCreators 對 this 透明化處理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.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。這如此的期望下,這樣的改動無疑是必須的。
對 state 的凍結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 是一個非常有趣的概念。這次改動圍繞判斷 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團隊設計思想
React 應用設計之道 - curry 化妙用
組件復用那些事兒 - React 實現按需加載輪子
通過實例,學習編寫 React 組件的“最佳實踐”
React 組件設計和分解思考
從 React 綁定 this,看 JS 語言發展和框架設計
做出Uber移動網頁版還不夠 極致性能打造才見真章**
React+Redux打造“NEWS EARLY”單頁應用 一個項目理解最前沿技術棧真諦
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/52247.html
摘要:在幾天前發布了新版本,被合入。但是在版本迭代的背后很多有趣的設計值得了解。參數處理這項改動由提出。對透明化處理中的,達到將包裹起來的目的。對的凍結認為,在中使用和方法是一種反模式。尤其是這樣的新,某些開發者認為將逐漸取代。 showImg(https://segmentfault.com/img/remote/1460000014571148); Redux 在幾天前(2018.04....
摘要:技術前端布局推進劑間距規范化利用變量實現令人震驚的懸浮效果很棒,但有些情況不適用布局說可能是最全的圖片版學習網格布局使用的九大誤區圖解布局布局揭秘和中新增功能探索版本迭代論基礎談展望對比探究繪圖中撤銷功能的實現方式即將更改的生命周期幾道高 技術 CSS 前端布局推進劑 - 間距規范化 利用CSS變量實現令人震驚的懸浮效果 Flexbox 很棒,但有些情況不適用 CSS布局說——可能是最...
摘要:精讀前端可以從多個角度理解,比如規范框架語言社區場景以及整條研發鏈路。同是前端未來展望,不同的文章側重的格局不同,兩個標題相同的文章內容可能大相徑庭。作為使用者,現在和未來的主流可能都是微軟系,畢竟微軟在操作系統方面人才儲備和經驗積累很多。 1. 引言 前端展望的文章越來越不好寫了,隨著前端發展的深入,需要擁有非常寬廣的視野與格局才能看清前端的未來。 筆者根據自身經驗,結合下面幾篇文章...
閱讀 2609·2021-11-17 17:00
閱讀 1864·2021-10-11 10:57
閱讀 3716·2021-09-09 11:33
閱讀 911·2021-09-09 09:33
閱讀 3550·2019-08-30 14:20
閱讀 3312·2019-08-29 11:25
閱讀 2796·2019-08-26 13:48
閱讀 734·2019-08-26 11:52