背景
redux作为前端状体管理中最亮眼的那个仔,非常有必要弄清楚他的原理。本文将从源码结合实践一起来重新认识redux。纯干货分享!!!
下面将会针对redux的实现原理以及最核心的中间件原理进行讲述,读者要重点关注
- createStore的返回值
- enhancer强大之处及其实现applyMiddleware的使用
- compose函数和middleware的原理和写法
- dispatch设计与中间件的巧妙之处
export const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
);
我们经常看到这段代码,本文将从以createStore作为入口顺藤摸瓜带你认识整个框架。下面源码是v3.7.2版本的代码。
createStore
首先来看createStore函数源码, 为了方便理解和阅读省略了很多无关的代码,大家在阅读的时候可以折叠起来看。
export default function createStore(reducer, preloadedState, enhancer) {
// 如果只有两个参数,并且第二个参数是函数的话,将会传递给enhancer
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
// 省略一堆判断逻辑
return enhancer(createStore)(reducer, preloadedState)
}
// 一堆方法定义
dispatch({ type: ActionTypes.INIT });
return {
dispatch, // 重点讲述
subscribe, // 重点讲述
getState, // 返回state的方法
replaceReducer, // 高级用法,目的在于分包时,动态更换reducer
[?observable]: observable
}
}
- 从代码中可以看到store的返回值是一个对象,具有多种方法
- enhancer的作用是功能扩展,返回值是一个store, enhancer函数的写法举例
function myEnhancer(createStore){
return (reducer, preloadedState, enhancer) => {
//创建store之前, do someSting
const store = createStore(reducer, preloadedState, enhancer)
//store之后, do something
return store;
}
}
- store创建之后,就会dispatch一个默认的初始action,来做初始化。这步操作可以类比与函数自执行,目的是为了让每个reducer返回他们默认的state构成初始全局state。
- 全局state其实就是一个普通对象函数,其他操作都是来辅助管理该state
dispatch
dispatch是我们的重头戏,后面还是介绍他,我们先看下,当我们dispatch({ type: 'INCREACE', payload: 1})会发生些什么呢。
function dispatch(action) {
// 各种检查acton类型
try {
isDispatching = true
// currentState是原来的state
// currentReducer就是一开始createStore时传入的reducer
currentState = currentReducer(currentState, action)
// reducer之后返回的是更新后的新的state
} finally {
isDispatching = false
}
// 更新监听者函数
const listeners = currentListeners = nextListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
- dispatch触发一个action,执行reducer,然后更新监听者,最后返回action本身。这里为什么要返回action呢?答案是为了中间件的链式复合,在中间件部分会详细解释。
- action的类型检查中要求,action必须是一个普通对象,必须有type属性
- reducer是一个函数,接收两个参数state和action,并返回新的state,初始化时,state可能是undefined,因此通过触发默认action,来返回reducer的初始state。reducer常见格式如下:
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
text: action.text,
completed: false
}
]
case 'COMPLETE_TODO':
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
default:
return state
}
}
- reducer执行完成后,更新state,然后触listeners函数,没有任何参数,其中一个的应用是react-redux中的高阶组件connect更新机制,后面会深入解析react-redux,请持续关注哦!
subscribe
subscribe是一个简单的监听者模式,该函数主要是收集监听者。源码很简单如下
function subscribe(listener) {
// 检查listener类型
let isSubscribed = true
ensureCanMutateNextListeners()
// 该函数会复制一份currentListeners
// 保障更新期间其他listener不受影响
nextListeners.push(listener)
return function unsubscribe() {
if (!isSubscribed) {
return
}
// 省略部分错误检查
isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
// 下次运行时 currentListeners会重新从nextListeners中取,可以看dispatch的代码
// 作者这样做的目的主要是为了防止dispatch执行期间发生subscribe或者unsubscribe引发异常错误
}
}
到这里,整个redux的核心功能就介绍的差不多了,但是redux的威力并没有体现出来,接下来我们将介绍redux的扩展功能中间件。
applyMiddleware
该函数是一个enhancer函数,由redux实现提供, 用来嵌入中间件,也是我们经常使用的一个工具函数。
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
// 特别注意这个dispatch是使用let赋值的
// 这个预定义是为了防止用户提前使用,此时无法触发其他中间件
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)
// 这个dispatch方法不能在next函数前使用
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
- 输入参数是一些列中间件,返回值是一个store对象(可以对照createStore的代码),dispatch函数进行了封装。
- compose函数实现了一个简单的洋葱模型,上一个函数的输入作为下一个函数的输出,后面会详细介绍。
- 从代码中发现,每个中间件会输入getState和dispatch对象,返回值需要满足compose函数要求。举例如下,下面例子中可以记录每个action到更新state所花费的时间。
function loggerMiddleware({getState, dispatch}){ // 这部分对应的是middleware(middlewareAPI)
// 这块区域不能使用dispatch函数,否则会抛出错误!!
return next => action => {
console.time(action.type);
const result = next(action);
// result 对象是一个action类型的对象,如果中间件未修改过该值,则全等,一般来讲,action不应该被修改
console.timeEnd(action.type);
return result; // 将会传入下一个中间中
}
}
在书写中间件的时候,我们发现内部闭包了多个函数,如果部分函数采用async等方式的话,就可以实现异步操作,解决副作用的问题,redux-thunk正是借用这种方式实现,感兴趣的同学可以学习下,代码只有14行,这里就不展开讨论了。
- next函数是上一个中间件的返回值,是上一个中间件封装后返回的dispatch,next(action)的作用相当于dispatch(action),他会触发后续的中间件,因此next命名比较形象
compose
compose是一个函数构造器,返回一个新的函数。类似数学中的函数f(x),g(x),h(x)复合为f(g(h(x)))。上一个函数的输出作为下一个函数的输入。
export default 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)))
}
- 出于js单个返回值的限制,每个函数的参数只能有一个
- 如果参数x是一个值的时候,compose函数执行后也会得到一个值, 即 compose(a,b,c)(x)也会返回一个值。举例(九折后满500再减50):
function couponA(next) {
if(next >= 500){
return next - 50;
}
return next;
}
function couponB(next){
return next * 0.9;
}
const discount = compose(couponA, couponB);
discount(1000); // 850
当参数是一个值的时候,无法实现回旋镖的形式。上述例子其实是一个简单的职责链模式,感兴趣的可以深入挖掘,在电商打折规则中特别实用
- 如果参数是一个函数的时候,每个中间件也返回一个函数,如applyMiddleware中的dispatch。由于dispatch是一个函数,可以利用函数调用时执行的特点,实现回旋镖型的中间件,如上述loggerMiddleware,可以记录dispatch所花费的时间。
- compose函数中处理函数是从右向左执行,即最后一个函数先执行。
combineReducers
这是一个工具函数,可以将多个reducer聚合起来,返回值是一个reducer(这是一个函数)
// reducers是一个
export default function combineReducers(reducers) {
// 省略对reducers做了一堆检查
// 下面这句是为了好理解,我杜撰的,非真实源码
const finalReducers = {...reducers}
const finalReducerKeys = Object.keys(finalReducers);
function combination(state = {}, action) {
// 此处省略了一些检查
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]
// 这里以key划分命名空间,previousStateForKey为指定key下的state
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
// 每个reducer都应该有返回值
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
- 使用combineReducers后,对应的state也具有与reducers对象具有相同的结构。
bindActionCreators
该函数是redux提供的一个工具函数,首先要弄清楚action和actionCreator的关系。action是一个普通对象,actionCreator是一个构造action对象的函数
bindActionCreator的目的是将actionCreator与dispatch结合构造出一个能够直接触发一系列变化的Action方法 bindActionCreators就是将多个actionCreator转化为Action方法
function bindActionCreator(actionCreator, dispatch) {
return (...args) => dispatch(actionCreator(...args))
}
export default function bindActionCreators(actionCreators, dispatch) {
// 省略一系列检查
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
}
在实践中,结合reat-redux的connect的第二个参数mapDispatchToProps为例,展示actionCreators转化为可以直接运行的方法。
const actionCreators = {
increase: (payload) => ({ type: 'INCREASE', payload }),
decrease: (payload) => ({ type: 'DECREASE', payload })
}
@connect(
state => state,
{...actionCreators}
/**实际执行了下述代码
dispatch => ({
actions: boundActionCreators(actionCreators, dispatch)
})
**/
)
class Counter {
render(){
<div>
<button onClick={() => this.props.actions.increase(1)}>increase</button>
<button onClick={() => this.props.actions.decrease(1)}>decrease</button>
</div>
}
}
总结
- redux管理状态是通过一个currentState对象来存储全局状态,但是将修改状态拆分为了dipatch(action)和reducer两部分,大大提高工具库的灵活性和想象空间。
- 理解并学会redux中间件的写法,更加深入了解compose函数
- redux相对比较复杂,但在其基础上衍生了大量的第三方工具库,足见其生命力,在实践中体会作者架构的深意。
- 为了便于理解,删除了很多类型判断,这些类型判断能够帮助开发者更好的调试代码,同样非常重要,大家在自己研究源码时,不要忽视这些细节。
- 文章中包含了自己大量的理解,描述和理解有不妥之处,请批评指正!!!