寫在前面
API 設計很精簡的庫,有一些精緻的小技巧和函數式的味道
一.結構
src/
│ applyMiddleware.js
│ bindActionCreators.js
│ combineReducers.js
│ compose.js
│ createStore.js
│ index.js
│
└─utils/
warning.js
index 暴露出所有 API:
export {
createStore, // 關鍵
combineReducers, // reducer 組合 helper
bindActionCreators, // wrap dispatch
applyMiddleware, // 中間件機制
compose // 送的,函數組合 util
}
最核心的兩個東西是 createStore 和 applyMiddleware,地位相當於 core 和 plugin
二.設計理念
核心思路與 Flux 相同:
(state, action) => state
在源碼(createStore/dispatch())中的體現:
try {
isDispatching = true
// 重新計算 state
// (state, action) => state 的 Flux 基本思路
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
把 currentState 和 action 傳入頂層 reducer,經 reducer 樹逐層計算得到新 state
沒有 dispatcher 的概念,每個 action 過來,都從頂層 reducer 開始流經整個 reducer 樹,每個 reducer 只關注自己感興趣的 action,製造一小塊 state,state 樹與 reducer 樹對應,reducer 計算過程結束,就得到了新的 state,丟棄上一個 state
P.S.關於 Redux 的更多設計理念(action, store, reducer 的作用及如何理解),請查看 Redux
三.技巧
minified 檢測
function isCrushed() {}
// min 檢測,在非生產環境使用 min 的話,警告一下
if (
process.env.NODE_ENV !== 'production' &&
typeof isCrushed.name === 'string' &&
isCrushed.name !== 'isCrushed'
) {
// warning(...)
}
代碼混淆會改變 isCrushed 的 name,作為檢測依據
無干擾 throw
// 小細節,開所有異常都斷點時能追調用棧,不開不影響
// 生產環境也可以保留
try {
throw new Error('err')
} catch(e) {}
對比 velocity 裡用到的 異步 throw 技巧:
/!!! 技巧,異步 throw,不會影響邏輯流程
setTimeout(function() {
throw error;
}, 1);
同樣都不影響邏輯流程,無干擾 throw 的好處是不會丟失調用棧之類的上下文信息,具體如下:
This error was thrown as a convenience so that if you enable "break on all exceptions" in your console, it would pause the execution at this line.
master-dev queue
這個技巧沒有很合適的名字(master-dev queue 也是隨便起的,但比較形象),姑且叫它可變隊列:
// 2 個隊列,current 不能直接修改,要從 next 同步,就像 master 和 dev 的關係
// 用來保證 listener 執行過程不受干擾
// 如果 subscribe() 時 listener 隊列正在執行的話,新註冊的 listener 下一次才生效
let currentListeners = []
let nextListeners = currentListeners
// 把 nextListeners 作為備份,每次只修改 next 數組
// flush listener queue 之前同步
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
寫和讀要做一些額外的操作:
// 寫
ensureCanMutateNextListeners();
updateNextListeners();
// 讀
currentListeners = nextListeners;
相當於寫的時候新開個 dev 分支(沒有的話),讀的時候把 dev merge 到 master 並刪除 dev 分支
用在 listener 隊列場景非常合適:
// 寫(訂閱/取消訂閱)
function subscribe(listener) {
// 不允許空降
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
// 不允許跳車
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
// 讀(flush queue 執行所有 listener)
// 同步兩個 listener 數組
// flush listener queue 過程不受 subscribe/unsubscribe 干擾
const listeners = currentListeners = nextListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
可以類比開車的情景:
nextListeners 是候車室,開車前帶走候車室所有人,關閉候車室
車開走後有人要上車(subscribe())的話,新開一個候車室(slice())
人先進候車室,下一趟才帶走,不允許空降
下車時也一樣,車沒停的話,先通過候車室記下誰要下車,下一趟不帶他了,不允許跳車
很有意思 的技巧,與 git 工作流神似
compose util
function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
用來實現函數組合:
compose(f, g, h) === (...args) => f(g(h(...args)))
核心是 reduce(即 reduceLeft),具體過程如下:
// Array reduce API
arr.reduce(callback(accumulator, currentValue, currentIndex, array)[, initialValue])
// 輸入 -> 輸出
[f1, f2, f3] -> f1(f2(f3(...args)))
1. 做 ((a, b) => (...args) => a(b(...args)))(f1, f2)
得到 accumulator = (...args) => f1(f2(...args))
2. 做 ((a, b) => (...args) => a(b(...args)))(accumulator, f3)
得到 accumulator = (...args) => ((...args) => f1(f2(...args)))(f3(...args))
得到 accumulator = (...args) => f1(f2(f3(...args)))
注意兩個順序:
參數求值從內向外:f3-f2-f1 即從右向左
函數調用從外向內:f1-f2-f3 即從左向右
applyMiddleware 部分有用到這種順序,在參數求值過程 bind next(從右向左),在函數調用過程 next() 尾觸發(從左向右)。所以中間件長的比較奇怪:
// 中間件結構
let m = ({getState, dispatch}) => (next) => (action) => {
// todo here
return next(action);
};
是有原因的
充分利用自身機制
起初比較疑惑的一點是:
function createStore(reducer, preloadedState, enhancer) {
// 計算第一個 state
dispatch({ type: ActionTypes.INIT })
}
明明可以直接點,比如 store.init(),為什麼自己還非得走 dispatch?實際上有 2 個作用:
-
特殊
type在combineReducer中用作reducer返回值合法性檢查,作為一個簡單action用例 -
並標誌著此時的
state是初始的,未經reducer計算
reducer 合法性檢查時直接把這個初始 action 丟進去執行了 2 遍,省了一個 action case,此外還省了初始環境的標識變量和額外的 store.init 方法
充分利用了自身的 dispatch 機制,相當聰明的做法
四.applyMiddleware
這一部分源碼被 challenge 最多,看起來比較迷惑,有些難以理解
再看一下中間件的結構:
// 中間件結構
// fn1 fn2 fn3
let m = ({getState, dispatch}) => (next) => (action) => {
// todo here
return next(action);
};
怎麼就非得用個這麼醜的高階函數?
function applyMiddleware(...middlewares) {
// 給每一個 middleware 都注入{getState, dispatch} 剝掉 fn1
chain = middlewares.map(middleware => middleware(middlewareAPI))
// fn = compose(...chain) 是 reduceLeft 從左向右鏈式組合起來
// fn(store.dispatch) 把原始 dispatch 傳進去,作為最後一個 next
// 參數求值過程從右向左注入 next 剝掉 fn2,得到一系列 (action) => {} 的標準 dispatch 組合
// 調用被篡改過的 disoatch 時,從左向右傳遞 action
// action 先按 next 鏈順序流經所有 middleware,最後一環是原始 dispatch,進入 reducer 計算過程
dispatch = compose(...chain)(store.dispatch)
}
重點關注 fn2 是怎樣被剝掉的:
// 參數求值過程從右向左注入 next 剝掉 fn2 dispatch = compose(...chain)(store.dispatch)
如註釋:
-
fn = compose(...chain)是 reduceLeft 從左向右鏈式組合起來 -
fn(store.dispatch)把原始dispatch傳進去,作為最後一個next(最內層參數) -
上一步參數求值過程從右向左注入 next 剝掉 fn2
利用 reduceLeft 參數求值過程 bind next
再看調用過程:
-
調用被篡改過的
disoatch時,從左向右傳遞action -
action先按next鏈順序流經所有middleware,最後一環是原始dispatch,進入reducer計算過程
所以中間件結構中高階函數每一層都有特定作用:
fn1 接受 middlewareAPI 注入
fn2 接受 next bind
fn3 實現 dispatch API(接收 action)
applyMiddleware 將被重構,更清楚的版本見 pull request#2146,核心邏輯就是這樣,重構可能會考慮要不要做 break change,是否支持邊界 case,夠不夠易讀(很多人關注這幾行代碼,相關 issue/pr 至少有幾十個)等等,Redux 維護團隊比較謹慎,這塊的迷惑性被質疑了非常多次才決定要重構
五.源碼分析
Git 地址:https://github.com/ayqy/redux-3.7.0
P.S.註釋足夠詳盡。雖然最新的是 3.7.2 了,但不會有太大差異,4.0 可能有一波蓄謀已久的變化
暫無評論,快來發表你的看法吧