Redux中的程式設計藝術
Redux原始碼分析已經滿大街都是了。但是大多都是介紹如何實現,實現原理。而忽略了Redux程式碼中隱藏的知識點和藝術。為什麼稱之為藝術,是這些簡短的程式碼蘊含著太多前端同學應該掌握的
JS
知識以及巧妙的設計模式的運用。
createStore 不僅僅是一個API
... export default function createStore(reducer, preloadedState, enhancer) { ... let currentReducer = reducer let currentState = preloadedState let currentListeners = [] let nextListeners = currentListeners let isDispatching = false function ensureCanMutateNextListeners() { ... } function getState() { ... return currentState } function subscribe(listener) { ... } function dispatch(action) { ... return action } function replaceReducer(nextReducer) { ... } function observable() { ... } dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } } 複製程式碼
這段程式碼,蘊含著很多知識。
首先是通過閉包對內部變數進行了私有化,外部是無法訪問閉包內的變數。其次是對外暴露了介面來提供外部對內部屬性的訪問。這其實是典型的“沙盒模式”。
沙盒模式幫我們保護內部資料的安全性,在沙盒模式下,我們只能通過return
出來的開放接口才能對沙盒內部的資料進行訪問和操作。
雖然屬性被保護在沙盒中,但是由於JS語言的特性,我們無法完全避免使用者通過引用去修改屬性。
subscribe/dispatch 訂閱釋出模式
subscribe 訂閱
Redux
通過subscribe
介面註冊訂閱函式,並將這些使用者提供的訂閱函式新增到閉包中的nextListeners
最巧妙的是考慮到了會有一部分開發者會有取消訂閱函式的需求,並提供了取消訂閱的介面。
這個介面的'藝術'並不僅僅是實現一個訂閱模式,還有作者嚴謹的程式碼風格。
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
複製程式碼
充分考慮到入參的正確性,以及通過isDispatching
和isSubscribed
來避免意外發生。
其實這個實現也是一個很簡單的高階函式
的實現。是不是經常在前端面試題裡面看到?(T_T)
這讓我想起來了。很多初級,中級前端工程師呼叫完
addEventListener
就忘記使用removeEventListener
最終導致很多閉包錯誤。所以,記得在不在使用的時候取消訂閱是非常重要的。
dispatch 釋出
通過Redux
的dispatch
介面,我們可以釋出一個action物件,去通知狀態需要做一些改變。
同樣在函式的入口就做了嚴格的限制:
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
複製程式碼
不得不說,作者在程式碼健壯性的考慮是非常周全的,真的是自嘆不如,我現在基本上是隻要自己點不出來問題就直接提測。 (T_T)
下面的程式碼更嚴謹,為了保障程式碼的健壯性,以及整個Redux
的Store
物件的完整性。直接使用了try { ... } finally { ... }
來保障isDispatching
這個內部全域性狀態的一致性。
再一次跪服+掩面痛哭 (T_T)
後面就是執行之前新增的訂閱函式。當然訂閱函式是沒有任何引數的,也就意味著,使用者必須通過store.getState()
來取得最新的狀態。
observable 觀察者
從函式字面意思,很容易猜到observable
是一個觀察者模式的實現介面。
function observable() {
const outerSubscribe = subscribe
return {
subscribe(observer) {
if (typeof observer !== 'object' || observer === null) {
throw new TypeError('Expected the observer to be an object.')
}
function observeState() {
if (observer.next) {
observer.next(getState())
}
}
observeState()
const unsubscribe = outerSubscribe(observeState)
return { unsubscribe }
},
[$$observable]() {
return this
}
}
}
複製程式碼
在開頭,就將訂閱介面進行了攔截,然後返回一個新的物件。這個物件為使用者提供了新增觀察物件的介面,而這個觀察物件需要具有一個next
函式。
combineReducers 又雙叒叕見“高階函式”
function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
if (process.env.NODE_ENV !== 'production') {
if (typeof reducers[key] === 'undefined') {
warning(`No reducer provided for key "${key}"`)
}
}
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
let unexpectedKeyCache
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}
let shapeAssertionError
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
return function combination(state = {}, action) {
if (shapeAssertionError) {
throw shapeAssertionError
}
if (process.env.NODE_ENV !== 'production') {
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
warning(warningMessage)
}
}
let hasChanged = false
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
複製程式碼
再一次被作者的嚴謹所折服,從函式開始就對引數的有效性進行了檢查,並且只有在非生產模式才進行這種檢查。並在assertReducerShape
中對每一個註冊的reducer
進行了正確性的檢查用來保證每一個reducer
函式都返回非undefined
值。
哦!老天,在返回的函式中,又進行了嚴格的檢查(T_T)。然後將每一個reducer
的返回值重新組裝到新的nextState
中。並通過一個淺比較來決定是返回新的狀態還是老的狀態。
bindActionCreators 還是高階函式
function bindActionCreator(actionCreator, dispatch) {
return function() {
return dispatch(actionCreator.apply(this, arguments))
}
}
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error(
`bindActionCreators expected an object or a function, instead received ${
actionCreators === null ? 'null' : typeof actionCreators
}. ` +
`Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
)
}
const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
複製程式碼
我平時是很少用這個API
的,但是這並不阻礙我去欣賞這段程式碼。可能這裡是我唯一能夠吐槽大神的地方了for (let i = 0; i < keys.length; i++) {
,當然他在這裡這麼用其實並不會引起什麼隱患,但是每次迴圈都要取一次length
也是需要進行一次多餘計算的(^_^)v,當然上面程式碼也有這個問題。
其實在開始位置的return dispatch(actionCreator.apply(this, arguments))
的apply(this)
的使用更是非常的666到飛起。
一般我們會在元件中這麼做:
import { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as TodoActionCreators from './TodoActionCreators'
console.log(TodoActionCreators)
class TodoListContainer extends Component {
componentDidMount() {
let { dispatch } = this.props
let action = TodoActionCreators.addTodo('Use Redux')
dispatch(action)
}
render() {
let { todos, dispatch } = this.props
let boundActionCreators = bindActionCreators(TodoActionCreators, dispatch)
console.log(boundActionCreators)
return <TodoList todos={todos} {...boundActionCreators} />
}
}
export default connect(
state => ({ todos: state.todos })
)(TodoListContainer)
複製程式碼
當我們使用bindActionCreators
建立action釋出函式的時候,它會自動將函式的上下文(this
)繫結到當前的作用域上。但是通常我為了解藕,並不會在action的釋出函式中訪問this
,裡面只存放業務邏輯。
再一個還算可以吐槽的地方就是對於Object的判斷,對於function的判斷重複出現多次。當然,單獨拿出來一個函式來進行呼叫,效能代價要比直接寫在這裡要大得多。
applyMiddleware 強大的聚合器
import compose from './compose'
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
複製程式碼
通過前面的程式碼,我們可以發現applayMiddleware
其實就是包裝enhancer
的工具函式,而在createStore
的開始,就對引數進行了適配。
通常我們會像下面這樣註冊middleware
:
const store = createStore(
reducer,
preloadedState,
applyMiddleware(...middleware)
)
複製程式碼
或者
const store = createStore(
reducer,
applyMiddleware(...middleware)
)
複製程式碼
所以,我們會驚奇的發現。哦,原來我們把applyMiddleware
呼叫放到第二個引數和第三個引數都是一樣的。所以我們也可以認為createStore
也實現了介面卡模式。當然,貌似有一些牽強(T_T)。
關於applyMiddleware
,也許最複雜的就是對compose
的使用了。
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
複製程式碼
通過以上程式碼,我們將所有傳入的middleware
進行了一次剝皮,把第一層高階函式返回的函式拿出來。這樣chain
其實是一個(next) => (action) => { ... }
函式的陣列,也就是中介軟體剝開後返回的函式組成的陣列。 然後通過compose
對中介軟體陣列內剝出來的高階函式進行組合形成一個呼叫鏈。呼叫一次,中介軟體內的所有函式都將被執行。
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製程式碼
因此經過compose
處理後,傳入中介軟體的next
實際上就是store.dispatch
。而這樣處理後返回的新的dispatch
,就是經過applyMiddleware
第二次剝開後的高階函式(action) => {...}
組成的函式鏈。而這個函式鏈傳遞給applyMiddleware
返回值的dispatch
屬性。
而通過applyMiddleware
返回後的dispatch
被返回給store
物件內,也就成了我們在外面使用的dispatch
。這樣也就實現了呼叫dispatch
就實現了呼叫所有註冊的中介軟體。
結束語
Redux的程式碼雖然只有短短几百行,但是蘊含著很多設計模式的思想和高階JS語法在裡面。每次讀完,都會學到新的知識。而作者對於高階函式的使用是大家極好的參考。
當然本人涉足JS
開發時間有限。會存在很多理解不對的地方,希望大咖指正。
作者:YaHuiLiang(Ryou)
連結:https://juejin.im/post/5b1fbd145188257d547217f4
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。