Redux 学习笔记 - 源码阅读

同步自个人 博客react

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

注意:本文不是单纯的讲 API,若是不了解的能够先看一下文档,或者 google 一下 Redux 相关的基础内容。github

总体架构

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

  1. store 负责存储数据redux

  2. 用户触发 action数组

  3. reducer 监听 action 变化,更新数据,生成新的 storepromise

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

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

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

index.js

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

这里有这么一段代码,主要是为了校验非生产环境下是否使用的是未压缩的代码,压缩以后,由于函数名会变化,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 等待后续更新。

相关文章
相关标签/搜索