Redux 学习笔记 - 源码阅读

1,379 阅读10分钟

同步自我的 博客

很久之前就看过一遍 Redux 相关技术栈的源码,最近在看书的时候发现有些细节已经忘了,而且发现当时的理解有些偏差,打算写几篇学习笔记。这是第一篇,主要记录一下我对 Redux 、redux-thunk 源码的理解。我会讲一下大体的架构,和一些核心部分的代码解释,更具体的代码解释可以去看我的 repo,后续会继续更新 react-redux,以及一些别的 redux 中间件的代码和学习笔记。

注意:本文不是单纯的讲 API,如果不了解的可以先看一下文档,或者 google 一下 Redux 相关的基础内容。

整体架构

在我看来,Redux 核心理念很简单

  1. store 负责存储数据
  2. 用户触发 action
  3. reducer 监听 action 变化,更新数据,生成新的 store

代码量也不大,源码结构很简单:

.src
    |- utils
    |- applyMiddleware.js
    |- bindActionCreators.js
    |- combineReducers.js
    |- compose.js
    |- createStore.js
    |- index.js

其中 utils 只包含一个 warning 相关的函数,这里就不说了,具体讲讲别的几个函数

index.js

这是入口函数,主要是为了暴露 ReduxAPI

这里有这么一段代码,主要是为了校验非生产环境下是否使用的是未压缩的代码,压缩之后,因为函数名会变化,isCrushed.name 就不等于 isCrushed

if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
    warning(...)
)}

createStore

这个函数是 Redux 的核心部分了,我们先整体看一下,他用到的思路很简单,利用一个闭包,维护了自己的私有变量,暴露出给调用方使用的 API

// 初始化的 action
export const ActionTypes = {
    INIT: '@@redux/INIT'
}

export default function createStore(reducer, preloadedState, enhancer) {

    // 首先进行各种参数获取和类型校验,不具体展开了
    if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
        enhancer = preloadedState
        preloadedState = undefined
  }
  if (typeof enhancer !== 'undefined') {...}
  if (typeof reducer !== 'function') {...}

  //各种初始化
  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  // 保存一份 nextListeners 快照,后续会讲到它的目的
  function ensureCanMutateNextListeners() {
        if (nextListeners === currentListeners) {
            nextListeners = currentListeners.slice()
        }
  }

  function getState(){...}

  function subscribe(){...}

  function dispatch(){...}

  function replaceReducer(){...}

  function observable(){...}

  // 初始化
  dispatch({ type: ActionTypes.INIT })

  return {
        dispatch,
        subscribe,
        getState,
        replaceReducer,
        [$$observable]: observable
  }
}

下面我们具体来说

ActionTypes

这里的 ActionTypes 主要是声明了一个默认的 action,用于 reducer 的初始化。

ensureCanMutateNextListeners

它的目的主要是保存一份快照,下面我们就讲讲 subscribe,以及为什么需要这个快照

subscribe

目的是为了添加一个监听函数,当 dispatch action 时会依次调用这些监听函数,代码很简单,就是维护了一个回调函数数组

function subscribe(listener) {
    // 异常处理
    ...

    // 标记是否有listener
    let isSubscribed = true

    // subscribe时保存一份快照
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

      // 返回一个 unsubscribe 函数
    return function unsubscribe() {
        if (!isSubscribed) {
            return
        }

        isSubscribed = false
        // unsubscribe 时再保存一份快照
        ensureCanMutateNextListeners()
        //移除对应的 listener
        const index = nextListeners.indexOf(listener)
        nextListeners.splice(index, 1)
    }
}

这里我们看到了 ensureCanMutateNextListeners 这个保存快照的函数,Redux 的注释里也解释了原因,我这里直接说说我的理解:由于我们可以在 listeners 里嵌套使用 subscribeunsubscribe,因此为了不影响正在执行的 listeners 顺序,就会在 subscribeunsubscribe 时保存一份快照,举个例子:

store.subscribe(function(){
    console.log('first');

    store.subscribe(function(){
        console.log('second');
    })    
})
store.subscribe(function(){
    console.log('third');
})
dispatch(actionA)

这时候的输出就会是

first
third

在后续的 dispatch 函数中,执行 listeners 之前有这么一句:

const listeners = currentListeners = nextListeners

它的目的则是确保每次 dispatch 时都可以取到最新的快照,下面我们就来看看 dispatch 内部做了什么。

dispatch

dispatch 的内部实现非常简单,就是将当前的 stateaction 传入 reducer,然后依次执行当前的监听函数,具体解析大概如下:

function dispatch(action) {
    // 这里两段都是异常处理,具体代码不贴了
    if (!isPlainObject(action)) {
        ...
    }
    if (typeof action.type === 'undefined') {
        ...
    }

    // 立一个标志位,reducer 内部不允许再dispatch actions,否则抛出异常
    if (isDispatching) {
        throw new Error('Reducers may not dispatch actions.')
    }

    // 捕获前一个错误,但是会将 isDispatching 置为 false,避免影响后续的 action 执行
    try {
         isDispatching = true
         currentState = currentReducer(currentState, action)
    } finally {
         isDispatching = false
    }

      // 这就是前面说的 dispatch 时会获取最新的快照
    const listeners = currentListeners = nextListeners

    // 执行当前所有的 listeners
    for (let i = 0; i < listeners.length; i++) {
        const listener = listeners[i]
        listener()
    }

    return action
}

这里有两点说一下我的看法:

  1. 为什么reducer 内部不允许再 dispatch actions?我觉得主要是为了避免死循环。
  2. 在循环执行 listeners 时有这么一段
const listener = listeners[i]
listener()

乍一看觉得会为什么不直接 listeners[i]() 呢,仔细斟酌一下,发现这样的目的是为了避免 this 指向的变化,如果直接执行 listeners[i](),函数里的 this 指向的是 listeners,而现在就是指向的 Window

getState

获取当前的 state,代码很简单,就不贴了。

replaceReducer

更换当前的 reducer,主要用于两个目的:1. 本地开发时的代码热替换,2:代码分割后,可能出现动态更新 reducer的情况

function replaceReducer(nextReducer) {
     if (typeof nextReducer !== 'function') {
        throw new Error('Expected the nextReducer to be a function.')
    }

      // 更换 reducer
    currentReducer = nextReducer
    // 这里会进行一次初始化
    dispatch({ type: ActionTypes.INIT })
}

observable

主要是为 observable 或者 reactive 库提供的 APIReux 内部并没有使用这个 API,暂时不解释了。

combineReducers

先问个问题:为什么要提供一个 combineReducers

我先贴一个正常的 reducer 代码:

function reducer(state,action){
    switch (action.type) {
        case ACTION_LIST:
        ...
        case ACTION_BOOKING:
        ...
    }
}

当代码量很小时可能发现不了问题,但是随着我们的业务代码越来越多,我们有了列表页,详情页,填单页等等,你可能需要处理 state.list.product[0].name,此时问题就很明显了:由于你的 state 获取到的是全局 state,你的取数和修改逻辑会非常麻烦。我们需要一种方案,帮我们取到局部数据以及拆分 reducers,这时候 combineReducers 就派上用场了。

源码核心部分如下:

export default function combineReducers(reducers) {
    // 各种异常处理和数据清洗
    ... 

    return function combination(state = {}, action) {

        const finalReducers = {};
        // 又是各种异常处理,finalReducers 是一个合法的 reducers map
        ...

        let hasChanged = false;
        const nextState = {};
        for (let i = 0; i < finalReducerKeys.length; i++) {
            const key = finalReducerKeys[i];
            const reducer = finalReducers[key];
            // 获取前一次reducer
            const previousStateForKey = state[key];
            // 获取当前reducer
            const nextStateForKey = reducer(previousStateForKey, action);

            nextState[key] = nextStateForKey;
            // 判断是否改变
            hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
        }
        // 如果没改变,返回前一个state,否则返回新的state
        return hasChanged ? nextState : state;
    }
}

注意这一句,每次都会拿新生成的 state 和前一次的对比,如果引用没变,就会返回之前的 state,这也就是为什么值改变后 reducer 要返回一个新对象的原因。

hasChanged = hasChanged || nextStateForKey !== previousStateForKey;

随着业务量的增大,我们就可以利用嵌套的 combineReducers 拼接我们的数据,但是就笔者的实践看来,大部分的业务数据都是深嵌套的简单数据操作,比如我要将 state.booking.people.name 置为测试姓名,因此我们这边有一些别的解决思路,比如使用高阶 reducer,又或者即根据 path 来修改数据,举个例子:我们会 dispatch(update('booking.people.name','测试姓名')),然后在 reducer 中根据 booking.people.name 这个 path 更改对应的数据。

compose

接受一组函数,会从右至左组合成一个新的函数,比如compose(f1,f2,f3) 就会生成这么一个函数:(...args) => f1(f2(f3(...args)))

核心就是这么一句

return funcs.reduce((a, b) => (...args) => a(b(...args)))

拿一个例子简单解析一下

[f1,f2,f3].reduce((a, b) => (...args) => a(b(...args)))

step1: 因为 reduce 没有默认值,reduce的第一个参数就是 f1,第二个参数是 f2,因此第一个循环返回的就是 (...args)=>f1(f2(...args)),这里我们先用compose1 来代表它

step2: 传入的第一个参数是前一次的返回值 compose1,第二个参数是 f3,可以得到此次的返回是 (...args)=>compose1(f3(...args)),即 (...args)=>f1(f2(f3(...args)))

bindActionCreator

简单说一下 actionCreator 是什么

一般我们会这么调用 action

dispatch({type:"Action",value:1})

但是为了保证 action 可以更好的复用,我们就会使用 actionCreator

function actionCreatorTest(value){
    return {
        type:"Action",
        value
    }
}

//调用时
dispatch(actionCreatorTest(1))

再进一步,我们每次调用 actionCreatorTest 时都需要使用 dispatch,为了再简化这一步,就可以使用 bindActionCreatoractionCreator 做一次封装,后续就可以直接调用封装后的函数,而不用显示的使用 dispatch了。

核心代码就是这么一段:

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}

下面的代码主要是对 actionCreators 做一些操作,如果你传入的是一个 actionCreator 函数,会直接返回一个包装过后的函数,如果你传入的一个包含多个 actionCreator 的对象,会对每个 actionCreator 都做一个封装。

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  //类型错误
  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      ...
    )
  }

  // 处理多个actionCreators
  var keys = Object.keys(actionCreators)
  var boundActionCreators = {}
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i]
    var actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

applyMiddleware

想一下这种场景,比如说你要对每次 dispatch(action) 都做一次日志记录,方便记录用户行为,又或者你在做某些操作前和操作后需要获取服务端的数据,这时可能需要对 dispatch 或者 reducer 做一些封装,redux 应该是想好了这种用户场景,于是提供了 middleware 的思路。

applyMiddleware 的代码也很精炼,具体代码如下:

export default function applyMiddleware(...middlewares) {
    return (createStore) => (reducer, preloadedState, enhancer) => {
        const store = createStore(reducer, preloadedState, enhancer)
        let dispatch = store.dispatch
        let chain = []

        const middlewareAPI = {
            getState: store.getState,
            dispatch: (action) => dispatch(action) 
        }

        chain = middlewares.map(middleware => middleware(middlewareAPI))

        dispatch = compose(...chain)(store.dispatch)

        return {
            ...store,
            dispatch
        }
    }
}

可以看到 applyMiddleware 内部先用 createStorereducer 生成了 store,之后又用 store 生成了一个 middlewareAPI,这里注意一下 dispatch: (action) => dispatch(action),由于后续我们对 dispatch 做了修改,为了保证所有的 middleware 中能拿到最新的 dispatch,我们用了闭包对它进行了一次包裹。

之后我们执行了

chain = middlewares.map(middleware => middleware(middlewareAPI))

生成了一个 middleware[m1,m2,...]

再往后就是 applyMiddleware 的核心,它将多个 middleWare 串联起来并依次执行

dispatch = compose(...chain)(store.dispatch)

compose 我们之前有讲过,这里其实就是 dispatch = m1(m2(dispatch))

最后,我们会用新生成的 dispatch 去覆盖 store 上的 dispatch

但是,在 middleware 内部究竟是如何实现的呢?我们可以结合 redux-thunk 的代码一起看看,redux-thunk 主要是为了执行异步操作,具体的 API 和用法可以看 github,它的源码如下:

function createThunkMiddleware(extraArgument) {
    return ({ dispatch, getState }) => next => action => {
        if (typeof action === 'function') {
            return action(dispatch, getState, extraArgument);
        }

        // 用next而不是dispatch,保证可以进入下一个中间件
        return next(action);
    };
}

这里有三层函数

  1. ({ dispatch, getState })=> 这一层对应的就是前面的 middleware(middlewareAPI)
  2. next=> 对应前面 compose 链的逻辑,再举个例子,m1(m2(dispatch)),这里 dispatchm2nextm2(dispatch) 返回的函数是 m1next,这样就可以保证执行 next 时可以进入下一个中间件
  3. action 这就是用户输入的 action

到这里,整个中间件的逻辑就很清楚了,这里还有一个点要注意,就是在中间件的内部,dispatchnext 是要注意区分的,前面说到了,next 是为了进入下一个中间件,而由于之前提到的 middlewareAPI 用到了闭包,如果在这里执行 dispatch 就会从最一开始的中间件重新再走一遍,如果 middleWare 一直调用 dispatch 就可能导致无限循环。

那么这里的 dispatch 的目的是什么呢?就我看来,其实就是取决与你的中间件的分发思路。比如你在一个异步 action 中又调用了一个异步 action,此时你就希望再经过一遍 thunk middleware,因此 thunk 中才会有 action(dispatch, getState, extraArgument),将 dispatch 传回给调用方。

小结

结合这一段时间的学习,读了第二篇源码依然会有收获,比如它利用函数式和 curry 将代码做到了非常精简,又比如它的中间件的设计,又可以联想到 AOPexpress 的中间件。

那么,redux 是如何与 react 结合的?promisesaga 又是如何实现的?与 thunk 相比有和优劣呢?后面会继续阅读源码,记录笔记,如果有兴趣也可以 watch 我的 repo 等待后续更新。