redux源码解析

上周六参加了一个公司的面试,因为是很长时间以来的第一次面试,发挥的并不是很好,有两个下来一想就明白的问题在当时却卡壳了,现在想来也是蛮遗憾的。

这篇文章也是基于其中的一个问题,一个关于redux源码的问题。面试之前对于redux源码的了解只是看过applyMiddleware.js这个文件,自然这个问题答得不是很好。我是个比较较真的人,晚上下班回来就翻开redux源码看了一遍,这篇文章也是就我的理解来粗浅分析一下redux的源码。

概述

如果你在用react,你一定会感到原生的react随着组件层级和数量的增多,组件间状态的管理会变得越来越难以管理和维护。而redux的出现就是为了解决这个问题,它把组件的状态统一维护成一个状态树,任何state的改变都通过redux提供的方法来触发,这样你只需要维护一颗树,更多麻烦的逻辑就不用操心了。当然,这是理想情况,但redux确实很大程度地方便了状态管理。不过关于redux使用的一些技巧和时机,这篇文章就不展开讲了有机会我会单独总结一篇redux使用中的一些经验和教训。
分析redux的源码,主要是了解其原理,观察其写法,然后方便我们更好的使用。

代码组成

redux的源码非常简单,主要文件只有几个,如下:


redux_source_code_structure
redux_source_code_structure

那么他们分别是干什么的呢?一个一个来看~

createStore.js


import isPlainObject from 'lodash/isPlainObject'
import $$observable from 'symbol-observable'

//redux自己创建的action,用来初始化状态树和reducer改变后初始化状态树
export const ActionTypes = {
  INIT: '@@redux/INIT'
}

export default function createStore(reducer, preloadedState, enhancer) {
  //如果第二个参数为方法且第三个参数为空,则将两个参数交换
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  //enhancer和reducer必须为function类型
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    //将enhancer包装一次createStore方法,再调用无enhancer的createStore方法
    return enhancer(createStore)(reducer, preloadedState)
  }

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

  let currentReducer = reducer //当前的reducer函数
  let currentState = preloadedState //当前的state树
  let currentListeners = []      //监听函数列表
  let nextListeners = currentListeners  //监听列表的一个引用
  let isDispatching = false      //是否正在dispatch

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  //下面的函数被return出去,函数作为返回值,则形成了闭包,currentState等状态会被保存
  
  //返回当前state树
  function getState() {
       //...
  }

  //添加注册一个监听函数,返回一个可以取消此监听的方法
  function subscribe(listener) {
       //...
  }

  function dispatch(action) {
      //...
  }

  //替换当前reducer
  function replaceReducer(nextReducer) {
      //...
  }

  function observable() {
      //...
  }

  // 当store被创建的时候,初始化状态树
  dispatch({ type: ActionTypes.INIT })

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

createStore接受三个参数,我想只要你用过,你并不会陌生。

  • reducer:这个参数是一个函数,返回下一个state树
  • preloadedState:preloadedState是初始的state
  • enhancer:第三个参数是一个store增强器,函数类型,只能使用'applyMiddleware'方法来生成

这个方法最终生成一个对象,对象中包含了一个重要的成员变量,即State树
暴露了几个成员方法

  • dispatch:redux中唯一触发state树修改的方法,分发一个action,然后在该action对应的处理函数中返回一个新的state树
  • subscribe:给store的状态树添加监听函数,一旦dispatch被调用,所有的监听函数就会被执行
  • getState:返回当前的状态树
  • replaceReducer:替换当前store中reducer的方法
  • [$$observable]:observable

上述代码中,可以看到createStore方法最终return了几个方法,通过闭包的原理,内部的各个变量也就被持久化的存储了,而通过暴露出的getState,dispatch等函数,我们可以获取或者改变其内部的state.那么暴露出来的这几个函数都是做什么的呢?

getState


function getState() {
    return currentState
}

这个方法就是用于返回当前的状态树。

subscribe


function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected listener to be a function.')
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

这个函数用于给store添加监听函数,把需要添加的监听函数作为参数传入即可,nextListeners 即为目前的监听函数列表,添加了之后,subscribe方法会返回一个unsubscribe()方法,此方法用于注销刚才添加的监听函数。

dispatch


function dispatch(action) {
    //action必须是一个包含type的对象
    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?'
      )
    }

    //如果正处于isDispatching状态,报错
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      //这里就是调用我们reducer方法的地方,返回一个新的state作为currentState
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    //调用所有的监听函数
    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

dispatch可能是我们用到的最多的redux方法,那么它做了什么呢?
非常简单! 它只是执行了当前的reducer方法,然后把当前的state和你在调用dispatch时传入的action作为参数,返回的值就是新的currentState。从这里我们也可以看出,改变state的代码逻辑就在reducer方法中,简单吧~
在这些执行完之后,dispatch方法会遍历当前的监听列表,并执行所有的监听函数。

replaceReducer


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

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.INIT })
 }

一目了然,没什么可说的。注意最后一句dispatch({ type: ActionTypes.INIT }),是替换reducer之后重新初始化状态树。

observable


function observable() {
    const outerSubscribe = subscribe
    return {
      subscribe(observer) {
        if (typeof observer !== 'object') {
          throw new TypeError('Expected the observer to be an object.')
        }

        function observeState() {
          //观察者模式的链式结构,传入当前的state
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }

这个方法用于提供观察者模式的操作,关于观察者模式,大家可以去了解一下。

compose.js


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)))  
}

非常简洁的代码。说一下最后一句代码,非常函数式的一句代码。
如果你使用的不多,你可能对reduce方法的作用有一些模糊,我先说一下reduce方法。
reduce方法接受2个参数,第一个参数是一个callback函数,第二个是一个初始值initValue
第一个函数有四个参数

  • previousValue: 上一次调用callback时返回的值
  • currentValue: 正在处理的数组元素
  • index: 正在处理的数组元素下标
  • array: 被处理的数组

如果有initValue,initValue将作为第一次的previousValue,若没有,则数组第一个元素将作为previousValue,后面一个元素将作为currentValue,然后执行callback的函数体,将返回的值作为previousValue,将下一个元素作为currentValue,一直到最后一个数组最后一个元素执行完位置,再返回最终的结果。
比如有一个数组arr=[1,2,3,4,5],我们使用reduce来求和:
let sum = [1,2,3,4,5].reduce((a,b)=>a+b);

好了,我就当作你已经理解了reduce方法,那么redux这里的compose也就不难理解了,它巧妙的地方在于数组的每个元素都是函数,callback返回一个复合函数作为previousValue,在reduce方法执行完之后,也就返回了一个将整个数组中所有函数串式调用的一个函数。

applyMiddleware.js


export default function applyMiddleware(...middlewares) {

  //return一个函数,它可以接受createStore方法作为参数,给返回的store的dispatch方法再进行一次包装
  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)
    }

    //传入middlewareAPI参数并执行每一个外部函数,返回结果汇聚成数组
    chain = middlewares.map(middleware => middleware(middlewareAPI))

    //这里用到了刚才说到的compose方法,如果你忘了,返回去看一下
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

还记得刚才createStore方法中的enhancer参数吗?applyMiddleware就是用来创建enhancer函数的。
官方的注释中提到了redux-thunk,就是使用applyMiddleware的一个很好的例子,我们结合它的代码来看可以更好的理解,下面是它的代码:


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

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

代码非常短,可以看到最终export了一个接受{ dispatch, getState }作为参数的function thunk,这个thunk方法也就是传给applyMiddleware方法的参数,此时的middlewares只有thunk一个方法,那么applyMiddleware中的chain也就很显然的是执行了thunk方法后返回的结果,我们再看redux-thunk的代码,返回了一个接受next作为参数的方法A! applyMiddleware的下一行,dispatch = compose(...chain)(store.dispatch),chain只有一个function,所以这里可以忽略compose,那么这一句就是将store.dispatch 作为next参数传给了刚才的方法A,终于,方法A返回了我们熟悉的dispatch方法。有点绕,但是我相信只要你静下心缕一缕,是可以弄明白的,然后你就会体会到函数式编程的魅力!
但是注意,此时的dispatch方法还是原来的dispatch方法吗?
人面不知何处去,桃花依旧笑春风。它已经不是原来的它了。经过thunk方法的包装,早已物是人非。
我们来看一下redux-thunk的代码,第三行之后的4行,如果dispatch方法接受的参数不是一个function,那么这个dispatch就和普通的dispatch没什么不同,但如果此时的action是一个方法,那么就会执行此方法,且第一个参数是store.dispatch。
这意味着我们的action创建函数不再只能创建一个包含type的Object,而可以是一个方法。你可能会问有什么用呢?当你在action中需要一个异步操作,并需要在回调中改变state的状态的时候,这就是一个绝佳的解决方案。

所以说,applyMiddleware实际上做了一件事,就是根据外部函数(中间件函数)包装原来的dispatch函数,然后将新的dispatch函数暴露出去。

再回头去看createStore.jsx中的 return enhancer(createStore)(reducer, preloadedState)这句代码,是不是明白了很多事情?

bindActionCreators.js


import warning from './utils/warning'

//很简单却很关键,我就不解释了~
function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}

  /**
   * 将action与dispatch函数绑定,生成直接可以触发action的函数,
   * 可以将第一个参数对象中所有的action都直接生成可以直接触发dispatch的函数
   * 而不需要一个一个的dispatch,生成后的方法对应原来action生成器的函数名
   * */
export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }
    
  //actionCreators必须为object类型
  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]
    
    //给actionCreators的每一个成员都绑定dispatch方法生成新的方法,
    //然后注入新的对象中,新方法对应的key即为原来在actionCreators的名字
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    } else {
      warning(`bindActionCreators expected a function actionCreator for key '${key}', instead received type '${typeof actionCreator}'.`)
    }
  }
  return boundActionCreators

这个方法主要的作用就是将action与dispatch函数绑定,生成直接可以触发action的函数。代码比较简单注释也比较明白,就过去了~

combineReducers.js


import { ActionTypes } from './createStore'
import isPlainObject from 'lodash/isPlainObject'
import warning from './utils/warning'

//根据key和action生成错误信息
function getUndefinedStateErrorMessage(key, action) {
  //...
}

//一些警告级别的错误
function getUnexpectedStateShapeWarningMessage(inputState, reducers, action, unexpectedKeyCache) {
  const reducerKeys = Object.keys(reducers)
  const argumentName = action && action.type === ActionTypes.INIT ?
    'preloadedState argument passed to createStore' :
    'previous state received by the reducer'

  //判断reducers是否为空数组
  //判断state是否是对象
  //给state中存在而reducer中不存在的属性添加缓存标识并警告
  //...
}


//这个方法用于检测用于组合的reducer是否是符合redux规定的reducer
function assertReducerSanity(reducers) {
  Object.keys(reducers).forEach(key => {
    const reducer = reducers[key]
    //调用reducer方法,undefined为第一个参数
    //使用前面说到过的ActionTypes.INIT和一个随机type生成action作为第二个参数
    //若返回的初始state为undefined,则这是一个不符合规定的reducer方法,抛出异常
    //...
  })
}

export default 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}"`)
      }
    }

    //finalReducers是过滤后的reducers,它的每一个属性都是一个function
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }

  const finalReducerKeys = Object.keys(finalReducers)

  let unexpectedKeyCache
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  let sanityError

  //检测每个reducer是否是符合标准的reducer
  try {
    assertReducerSanity(finalReducers)
  } catch (e) {
    sanityError = e
  }

  return function combination(state = {}, action) {
    if (sanityError) {
      throw sanityError
    }

    //如果不是成产环境,做一些警告判断
    if (process.env.NODE_ENV !== 'production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache)
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    let hasChanged = false
    const nextState = {} //下一个state树

    //遍历所有reducers,然后将每个reducer返回的state组合起来生成一个大的状态树,所以任何action,redux都会遍历所有的reducer
    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)

      //如果此reducer返回的新的state是undefined,抛出异常
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    //如果当前action对应的reducer方法执行完后,该处数据没有变化,则返回原来的流程树
    return hasChanged ? nextState : state
  }
}

如果你没用过redux,或者只是看了一些它的介绍,你可能会困惑,将所有组件的状态变成一颗状态树,然后每次dispatch都返回一个新的状态树,这样的操作难道不会很麻烦吗?
这是redux的理念,但具体实现起来,当然可以通过聪明的方式来将它变得方便。combineReducers方法,就是一个将状态树管理变得清晰简单的方法。它可以把多个reducer函数组合起来,然后返回一个整的、新的reducer函数。

可以看到它接受一个参数,这个参数是一个对象,对象的每一个属性都是一个reducer,key是组合后的属性名,value则是一个reducer函数。
举个例子,如果reducers参数为
{userList:getUserListReducers,foodMenu:getMenuReducer}
那么最终生成的store就会是
{userList:{...},foodMenu:{...}}
两个{...}是通过上面的getUserListReducers和getMenuReducer方法计算得到的。

结尾

至此,redux的源码就分析完了,容我们感叹一句:这代码真是太漂亮了!
具体使用中,redux还有一个好兄弟叫做react-redux,下次我们再来讲讲它~

本文已同步到github 上~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • 写在开始 本篇主要结合react-native 使用redux的过程,说明使用redux的方法和原理,揭秘Redu...
    素月某某阅读 1,556评论 0 5
  • http://gaearon.github.io/redux/index.html ,文档在 http://rac...
    jacobbubu阅读 79,930评论 35 198
  • 一、什么情况需要redux? 1、用户的使用方式复杂 2、不同身份的用户有不同的使用方式(比如普通用户和管...
    初晨的笔记阅读 2,016评论 0 11
  • 学习必备要点: 首先弄明白,Redux在使用React开发应用时,起到什么作用——状态集中管理 弄清楚Redux是...
    贺贺v5阅读 8,880评论 10 58
  • 看到这篇文章build an image gallery using redux saga,觉得写的不错,长短也适...
    smartphp阅读 6,148评论 1 29