從0實現一個tinyredux
從0實現一個tinyredux
講真,redux已經很小了,去掉注釋代碼也就300行吧, 大家可以去讀一下, 注釋寫的也是非常詳細了。
redux 更多的是對思維上的變化:數據改變 + 視圖更新 二者分開,各自管理自己。現在,讓我們從無到有!!
so tiny !
redux 是這樣的一個流程:觸發一個action → redux做一些邏輯,返回state → 觸發監聽程序。 這不就是圖形界面的事件機制嗎(在web 上就是addEventListener)!
所以一個 最小的redux:class Store { constructor(reducer, state = {}) { this.state = state this.listeners = [] this.reducer = reducer } dispatch(action) { this.state = this.reducer(this.state, action) this.listeners.forEach(listener => listener()) } getState() { return this.state } subscribe(listener) { this.listeners.push(listener) }}
我們的這個 Store 和 redux的store提供了想的api:
- dispatch 觸發一個action
- getState 返回當前狀態
- subscribe 增加一個監聽器
讓我們用這個最小的例子實現一個 計數器在線地址
function reducer(state, action) { switch (action.type) { case addOne: { return { ...state, count: state.count + 1 } } default: { return state } }}const store = new Store(reducer, {count: 0})store.subscribe(() => { console.log(subscribe test:, store.getState())})store.dispatch({type: addOne})store.dispatch({type: addOne})
另一個靈魂 middleware
redux的中文文檔 上關於middleware的部分, 已經講的很好了。現在我們從另一個角度來看這個問題,
首先,middleware 是redux在dispatch前後,提供的擴展機制。 比如日誌功能, 需要在dispath一個action之前記錄一下狀態,然後reducer處理完邏輯之後, 再次記錄一下。 這不就是 面向切面編程嗎!時髦的AOP! 用java的話不管是 靜態代理還是動態代理, 寫起來都挺複雜的。 但是js實現 很簡單:function enhancer(originF) { return function(...args) { console.log(before) var result = originF(...args) console.log(after) return result }}
enhancer 方法接受一個方法A, 返回一個增強的方法B。 對B我們可以再次 增強,所以這裡是可以鏈式調用的:
var fEnhancer = function (originF) { return function (...args) { console.log(this is fEnhancer before) var r = originF(...args) console.log(this is fEnhancer after) return r }}var hEnhancer = function (originF) { return function (...args) { console.log(this is hEnhancer before) var r = originF(...args) console.log(this is hEnhancer after) return r }}var gEnhancer = function (originF) { return function (...args) { console.log(this is gEnhancer before) var r = originF(...args) console.log(this is gEnhancer after) return r }}function justPrint() { console.log(justPrint...)}fEnhancer(hEnhancer(gEnhancer(justPrint)))()
這個例子輸出在線地址:
this is fEnhancer beforethis is hEnhancer beforethis is gEnhancer beforejustPrint...this is gEnhancer afterthis is hEnhancer afterthis is fEnhancer after
對於 fEnhancer(hEnhancer(gEnhancer(justPrint))) 等效的寫法如下:
var enhancerArray = [gEnhancer, hEnhancer, fEnhancer]function enhancerFun(originF) { let of = originF enhancerArray.forEach(enhancer => { of = enhancer(of) }) return of}
更加流弊的寫法, 也就是redux的實現(巧妙的使用了數組的reduce方法):
var enhancerArray = [gEnhancer, hEnhancer, fEnhancer]function enhancerFun2(originF) { return enhancerArray.reduce((a, b) => (...args) => a(b(...args)))(originF)}
回到 redux, 需要我們增強的是dispatch, 所以只需要 enhancerFun(store.dispatch)。 這裡有兩個問題:
第一個問題 由於我們的dispatch裡面使用了 this, 而這個增強的調用: var r = originF() 這裡就丟掉了this。解決方法如下:class Store { constructor(reducer, state) { this.state = state this.listeners = [] this.reducer = reducer this.dispatch = this.dispatch.bind(this) this.getState = this.getState.bind(this) this.subscribe = this.subscribe.bind(this) } ...}
這樣在任何地方調用 store的方法, 都沒有問題了
第二個問題:在gEnhancer 裡面我們想要調用 store.getState() 來記錄 調用dispatch 前後的狀態怎麼辦? (我們不可能每次去import store吧, 因為在寫enhancer的時候,
可能壓根就不知道 store在哪裡呢。 ) 方法如下:var fEnhancer = function ({ getState, dispatch }) { return function (originF) { return function (...args) { console.log(this is fEnhancer before, getState()) var r = originF(...args) console.log(this is fEnhancer after, getState()) return r } }}
通過閉包的形式, 我們讓 fEnhancer 內部的邏輯 可以直接使用 getState。
那middleware是什麼呢? 這裡的fEnhancer就是標準的一個 redux middleware, 是的,redux-logger可以不用了, 讓我們用fEnhancer吧。 對應的 applyMiddleware:
function applyMiddleware(store, ...args) { console.log(args) const enArr = args.map(middleware => middleware({ getState: store.getState, dispatch: store.dispatch })) let of = store.dispatch enArr.forEach(en => { of = en(of) }) store.dispatch = of}
現在, 給我們開頭的reducer 增強一下吧!! 在線地址
輔助函數
到這裡, tineyredux其實已經結束了。 但是redux為了方便開發者 提供了兩個輔助函數: combineReducers 和 bindActionCreators。
bindActionCreators 就是在 原本調用 actionCreator的時候,默認幫你dispatch一下: actionCreator() ==》 store.dispatch(actionCreator())。也可以理解為 『增強』:function bindActionCreator(creator, dispatch) { return function (...args) { dispatch(creator(args)) // <---- 也可以理解為 增強 }}export default function bindActionCreators(creators, dispatch) { const keys = Object.keys(creators) const result = {} keys.forEach(key => { result[key] = bindActionCreator(creators[key], dispatch) }) return result}
combineReducers 是為了解決另外的痛點, 比如如下的store 和reducer:
{ clock: { count: 0 }, yk: { age: 0 } ...}function reducer(state, action) { switch (action.type) { case clock_add:... case clock_cnum... case yk_older: ... case yk_forever18: ... default: { return state } }}
大部分情況, 我們發現我們的應用,clock數據部分,對應clock自己的邏輯, yk數據部分的修改邏輯也只會關心自己(通常這都是2個頁面的數據了)。
所以這裡的一個 「大switch」 是可以切分的。function clockReducer(state, action) { switch (action.type) { case clock_addOne: ... case clock_cnum: ... default: { return state } }}function ykReducer(state, action) { switch (action.type) { case yk_older: ... case yk_forever18: ... default: { return state } }}function reducer(state, action) { return { clock: clockReducer(state, action), yk: ykReducer(state, action), }}
combineReducers 就是對小的reducer進行合併的:
function combineReducers(reducers) { return function (state, action) { const keys = Object.keys(reducers) const newState = {} keys.forEach(key => { newState[key] = reducers[key](state[key], action) }) return newState }}
題外話: 這裡的 combineReducers 如果小reducer特別多, 會有一些性能問題: 因為對於每一個 action,都是走了所有的reducer。 如果我們場景特殊,
是我們剛才說的 一塊數據的邏輯 只對於一個reducer, 可以使用下面的變種(只會執行一個reducer, 需要保證action前綴和store中key一致):function combineReducersVariant(reducers) { return function (state, action) { const lineIndex = action.type.indexOf("_") const actionKey = action.type.substring(0, lineIndex) const newS = reducers[actionKey](state[actionKey], action) return state[actionKey] === newS ? state : { ...state, [actionKey]: newS } }}
這裡有一個完整的保護 middleware, bindActionCreators, combineReducers 所有特性的完整的例子
代碼託管在git
安裝: npm install tiny-redux —save
推薦閱讀:
※如何理解 Vue.JS 2016年的 github 星標( Star )數量增長超過 React ?
※「進擊的React」專欄開通
※Youtube-dl-Download-Videos
※在用react的時候老大不讓用jquery,為什麼?
※mobx-react 原理解析(二)