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