Redux源碼分析已經滿大街都是了。可是大多都是介紹如何實現,實現原理。而忽略了Redux代碼中隱藏的知識點和藝術。爲何稱之爲藝術,是這些簡短的代碼蘊含着太多前端同窗應該掌握的
JS
知識以及巧妙的設計模式的運用。前端
...
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
}
}
複製代碼
這段代碼,蘊含着不少知識。react
首先是經過閉包對內部變量進行了私有化,外部是沒法訪問閉包內的變量。其次是對外暴露了接口來提供外部對內部屬性的訪問。這實際上是典型的「沙盒模式」。面試
沙盒模式幫咱們保護內部數據的安全性,在沙盒模式下,咱們只能經過return
出來的開放接口才能對沙盒內部的數據進行訪問和操做。redux
雖然屬性被保護在沙盒中,可是因爲JS語言的特性,咱們沒法徹底避免用戶經過引用去修改屬性。設計模式
Redux
經過subscribe
接口註冊訂閱函數,並將這些用戶提供的訂閱函數添加到閉包中的nextListeners
中。數組
最巧妙的是考慮到了會有一部分開發者會有取消訂閱函數的需求,並提供了取消訂閱的接口。安全
這個接口的'藝術'並不只僅是實現一個訂閱模式,還有做者嚴謹的代碼風格。前端工程師
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
複製代碼
充分考慮到入參的正確性,以及經過isDispatching
和isSubscribed
來避免意外發生。閉包
其實這個實現也是一個很簡單的高階函數
的實現。是否是常常在前端面試題裏面看到?(T_T)app
這讓我想起來了。不少初級,中級前端工程師調用完
addEventListener
就忘記使用removeEventListener
最終致使不少閉包錯誤。因此,記得在不在使用的時候取消訂閱是很是重要的。
經過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
是一個觀察者模式的實現接口。
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
函數。
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
中。並經過一個淺比較來決定是返回新的狀態仍是老的狀態。
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的判斷重複出現屢次。固然,單獨拿出來一個函數來進行調用,性能代價要比直接寫在這裏要大得多。
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
開發時間有限。會存在不少理解不對的地方,但願大咖指正。