Redux源码解读

前言

作为React全家桶的一份子,Redux为react提供了严谨周密的状态管理。但Redux本身是有点难度的,虽然学习了React也有一段时间了,自我感觉算是入了门,也知道redux的大概流程。但其背后诸如creatstore,applymiddleware等API背后到底发生了什么事情,我其实还是不怎么了解的,因此最近花了几天时间阅读了Redux的源码,写下文章纪录一下自己看源码的一些理解。

一、源码结构(redux4.0版本)

Redux是出了名的短小精悍(恩,这个形容很贴切),只有2kb大小,且没有任何依赖。它将所有的脏活累活都交给了中间件去处理,自己保持着很好的纯洁性。再加上redux作者在redux的源码上,也附加了大量的注释,因此redux的源码读起来还是不算难的。

先来看看redux的源码结构,也就是src目录下的代码:

源码结构

其中utils是工具函数,主要是作为辅助几个核心API,因此不作讨论。
(注:由于篇幅的问题,下面代码很多都删除了官方注释,和较长的warn)

二、具体组成

index.js是redux的入口函数具体代码如下:

2.1 index.js

import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'
import __DO_NOT_USE__ActionTypes from './utils/actionTypes'

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

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}

其中isCrushed函数是用于验证在非生产环境下 Redux 是否被压缩,如果被压缩就会给开发者一个 warn 的提示。

在最后index.js 会暴露 createStore, combineReducers, bindActionCreators, applyMiddleware, compose 这几个redux最主要的API以供大家使用。

2.2 creatStore

createStore函数接受三个参数:

  • reducer:是一个函数,返回下一个状态,接受两个参数:当前状态 和 触发的 action;
  • preloadedState:初始状态对象,可以很随意指定,比如服务端渲染的初始状态,但是如果使用 combineReducers 来生成 reducer,那必须保持状态对象的 key 和 combineReducers 中的 key 相对应;
  • enhancer:是store 的增强器函数,可以指定为中间件,持久化 等,但是这个函数只能用 Redux 提供的 applyMiddleware 函数来进行生成

下面就是creactStore的源码,由于整体源码过长,且 subscribe 和 dispatch 函数也挺长的,所以就将 subscribe 和 dispatch 单独提出来细讲。

 import $$observable from 'symbol-observable'

import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }
  // enhancer应该为一个函数
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    //enhancer 接受 createStore 作为参数,对  createStore 的能力进行增强,并返回增强后的  createStore 。
    //  然后再将  reducer 和  preloadedState 作为参数传给增强后的  createStore ,最终得到生成的 store
    return enhancer(createStore)(reducer, preloadedState)
  }
  // reducer必须是函数
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

 // 初始化参数
  let currentReducer = reducer   // 当前整个reducer
  let currentState = preloadedState   // 当前的state,也就是getState返回的值
  let currentListeners = []  // 当前的订阅store的监听器
  let nextListeners = currentListeners // 下一次的订阅
  let isDispatching = false // 是否处于 dispatch action 状态中, 默认为false

  // 这个函数用于确保currentListeners 和 nextListeners 是不同的引用
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // 返回state
  function getState() {
    if (isDispatching) {
      throw new Error(
        ......
      )
    }
    return currentState
  }

  // 添加订阅
  function subscribe(listener) {
  ......
    }
  }
// 分发action
  function dispatch(action) {
    ......
  }

  //这个函数主要用于 reducer 的热替换,用的少
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }
    // 替换reducer
    currentReducer = nextReducer
    // 重新进行初始化
    dispatch({ type: ActionTypes.REPLACE })
  }

  // 没有研究,暂且放着,它是不直接暴露给开发者的,提供了给其他一些像观察者模式库的交互操作。
  function observable() {
    ......
  }

  // 创建一个store时的默认state
  // 用于填充初始的状态树
  dispatch({ type: ActionTypes.INIT })

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

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

    if (isDispatching) {
      throw new Error(
        ......
      )
    }

    let isSubscribed = true
    // 如果 nextListeners 和 currentListeners 是一个引用,重新复制一个新的
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

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

      if (isDispatching) {
        throw new Error(
          .......
        )
      }
      
      isSubscribed = false
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      // 从nextListeners里面删除,会在下次dispatch生效
      nextListeners.splice(index, 1)
    }
  }

有时候有些人会觉得store.subscribe用的很少,其实不然,是react-redux隐式的为我们帮我们完成了这方面的工作。subscribe函数可以给 store 的状态添加订阅监听,一旦我们调用了 dispatch来分发action ,所有的监听函数就会执行。而 nextListeners 就是储存当前监听函数的列表,当调用 subscribe,传入一个函数作为参数时,就会给 nextListeners 列表 push 这个函数。同时调用 subscribe 函数会返回一个 unsubscribe 函数,用来解绑当前传入的函数,同时在 subscribe 函数定义了一个 isSubscribed 标志变量来判断当前的订阅是否已经被解绑,解绑的操作就是从 nextListeners 列表中删除当前的监听函数。

dispatch

dispatch是redux中一个非常核心的方法,也是我们在日常开发中最常用的方法之一。dispatch函数是用来触发状态改变的,他接受一个 action 对象作为参数,然后 reducer 就可以根据 action 的属性以及当前 store 的状态,来生成一个新的状态,从而改变 store 的状态;

function dispatch(action) {
    // action 必须是一个对象
    if (!isPlainObject(action)) {
      throw new Error(
        ......
      )
    }
    // type必须要有属性,不能是undefined
    if (typeof action.type === 'undefined') {
      throw new Error(
        ......
      )
    }
    // 禁止在reducers中进行dispatch,因为这样做可能导致分发死循环,同时也增加了数据流动的复杂度
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
//       当前的状态和 action 传给当前的reducer,用于生成最新的 state
      currentState = currentReducer(currentState, action)
    } finally {  
      // 派发完毕
      isDispatching = false
    }
    // 将nextListeners交给listeners
    const listeners = (currentListeners = nextListeners)
    // 在得到新的状态后,依次调用所有的监听器,通知状态的变更
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    return action
  }

其中 currentState = currentReducer(currentState, action);这里的 currentReducer 是一个函数,他接受两个参数:

  • 当前状态
  • action

然后返回计算出来的新的状态。

2.3 compose.js

compose 可以接受一组函数参数,从右到左来组合多个函数,然后返回一个组合函数。它的源码并不长,但设计的十分巧妙:


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

compose函数的作用其实其源码的注释里讲的很清楚了,比如下面这样:

compose(funcA, funcB, funcC)

其实它与这样是等价的:

compose(funcA(funcB(funcC())))

ompose 做的只是让我们在写深度嵌套的函数时,避免了代码的向右偏移。

2.4 applyMiddleware

applyMiddleware也是redux中非常重要的一个函数,设计的也非常巧妙,让人叹为观止。

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    // 利用传入的createStore和reducer和创建一个store
    const store = createStore(...args)
    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)
    }
    // 让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

通过上面的代码,我们可以看出 applyMiddleware 是个三级柯里化的函数。它将陆续的获得三个参数:第一个是 middlewares 数组,第二个是 Redux 原生的 createStore,最后一个是 reducer,也就是上面的...args;

applyMiddleware 利用 createStore 和 reducer 创建了一个 store,然后 store 的 getState 方法和 dispatch 方法又分别被直接和间接地赋值给 middlewareAPI 变量。

其中这一句我感觉是最核心的:

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

我特意将compose与applyMiddleware放在一块,就是为了解释这段代码。因此上面那段核心代码中,本质上就是这样的(假设...chain有三个函数):

dispatch = f1(f2(f3(store.dispatch))))

2.5 combineReducers

combineReducers 这个辅助函数的作用就是,将一个由多个不同 reducer 函数作为 value 的 object合并成一个最终的 reducer 函数,然后我们就可以对这个 reducer 调用 createStore 方法了。这在createStore的源码的注释中也有提到过。

并且合并后的 reducer 可以调用各个子 reducer,并把它们返回的结果合并成一个 state 对象。 由 combineReducers() 返回的 state 对象,会将传入的每个 reducer 返回的 state 按其传递给 combineReducers() 时对应的 key 进行命名。

下面我们来看源码,下面的源码删除了一些的检查判断,只保留最主要的源码:

export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  // 有效的 reducer 列表
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
  const finalReducerKeys = Object.keys(finalReducers)

// 返回最终生成的 reducer
  return function combination(state = {}, action) {
    let hasChanged = false
    //定义新的nextState
    const nextState = {}
    // 1,遍历reducers对象中的有效key,
    // 2,执行该key对应的value函数,即子reducer函数,并得到对应的state对象
    // 3,将新的子state挂到新的nextState对象上,而key不变
    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)
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
     // 遍历一遍看是否发生改变,发生改变了返回新的state,否则返回原先的state
    return hasChanged ? nextState : state
  }
}

2.6 bindActionCreators

bindActionCreators可以把一个 value 为不同 action creator 的对象,转成拥有同名 key 的对象。同时使用 dispatch 对每个 action creator 进行包装,以便可以直接调用它们。
bindActionCreators函数并不常用(反正我还没有怎么用过),惟一会使用到 bindActionCreators 的场景就是我们需要把 action creator 往下传到一个组件上,却不想让这个组件觉察到 Redux 的存在,并且不希望把 dispatch 或 Redux store 传给它。

// 核心代码,并通过apply将this绑定起来
function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
} 
// 这个函数只是把actionCreators这个对象里面包含的每一个actionCreator按照原来的key的方式全部都封装了一遍,核心代码还是上面的
export default function bindActionCreators(actionCreators, dispatch) {
  // 如果actionCreators是一个函数,则说明只有一个actionCreator,就直接调用bindActionCreator
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }
  // 如果是actionCreator是对象或者null的话,就会报错
  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
    ... ... 
  }
 // 遍历对象,然后对每个遍历项的 actionCreator 生成函数,将函数按照原来的 key 值放到一个对象中,最后返回这个对象
  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
}

小节

看一遍redux,感觉设计十分巧秒,不愧是大佬的作品。这次看代码只是初看,往后随着自己学习的不断深入,还需多加研究,绝对还能得到更多的体会。

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

推荐阅读更多精彩内容