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
开发时间有限。会存在很多理解不对的地方,希望大咖指正。