redux源码结合实践深入解析

1,237 阅读8分钟

背景

redux作为前端状体管理中最亮眼的那个仔,非常有必要弄清楚他的原理。本文将从源码结合实践一起来重新认识redux。纯干货分享!!!

下面将会针对redux的实现原理以及最核心的中间件原理进行讲述,读者要重点关注

  1. createStore的返回值
  2. enhancer强大之处及其实现applyMiddleware的使用
  3. compose函数和middleware的原理和写法
  4. 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>
    }
}

总结

  1. redux管理状态是通过一个currentState对象来存储全局状态,但是将修改状态拆分为了dipatch(action)和reducer两部分,大大提高工具库的灵活性和想象空间。
  2. 理解并学会redux中间件的写法,更加深入了解compose函数
  3. redux相对比较复杂,但在其基础上衍生了大量的第三方工具库,足见其生命力,在实践中体会作者架构的深意。
  4. 为了便于理解,删除了很多类型判断,这些类型判断能够帮助开发者更好的调试代码,同样非常重要,大家在自己研究源码时,不要忽视这些细节。
  5. 文章中包含了自己大量的理解,描述和理解有不妥之处,请批评指正!!!