從0實現一個tiny-redux
講真,redux已經很小了,去掉註釋程式碼也就300行吧,註釋寫的也是非常詳細了。 redux 更多的是對思維上的變化:資料改變 + 檢視更新 二者分開,各自管理自己。
現在,讓我們從無到有!!
so tiny !
redux 是這樣的一個流程:觸發一個action --> redux做一些邏輯,返回state --> 觸發監聽程式。 所以一個 最小的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 before
this is hEnhancer before
this is gEnhancer before
justPrint...
this is gEnhancer after
this is hEnhancer after
this 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的方法, this 都沒有問題了
第二個問題:在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 所有特性的完整的例子
安裝: npm install tiny-redux --save