redux源码阅读笔记(二)

建议:有 redux 的实践后再来看相关的文章。你需要先知道 redux 能让你做什么,才会激起对源码的欲望。

  • 推荐看看这篇文章 Redux 卍解,回顾一下 redux都给你提供了哪些 api ,能干些什么。
  • 不准备把行行代码都贴出来,建议自行打开源码同步阅读。

redux 的源码内容并不多,可以说很少,相比 koa.js 会多一点 (笑)。源码结构如下图:


源码结构

combineReducers.js

combineReducers的大致结构如下

image.png

回忆我们使用 combineReducers 的时候

//a.reducer
const aR =  function(state = initialState, action) {
  switch (action.type) {
  case ...
  default:
    return state;
  }
}
//b.reducer
const bR =  function(state = initialState, action) {
  switch (action.type) {
  case ...
  default:
     return state;
  }
}

export default combineReducers({aR,bR});

对应到源码,就不难理解使用时的一些“要求”。(比如 接受的参数对象 的 key 要与对应的 state 对应。)


image.png

代码先对 参数 reducers 进行一次遍历查空,值非空且为 function 的键值对组成最终的 recuder。题外话:现在遍历对象还是得靠 Object.keys 吗?

//assertReducerShape
  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}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }

按部就班下来,finalReducers 就是我们刚才得到的新对象,去往 assertReducerShape 内部,可以发现该函数内并没有做影响外部的事情,也没有返回值,该函数的用意应该是试探性的运行当前的 reducer,确定 reducer 被调用后没有返回 undefined ..而且还进行了两次判断...@todo 暂时还未明白为何要两次判断

let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

combineReducers 的返回值仍然是函数,毕竟我们只是将多个 reducer 函数合并成一个函数;核心逻辑代码如下,可以看出 进入 combineReducers 的 reducers 的 key 是需要和 state 下对应 key 一致的;赋值给 nextState 前还需要判断非 undefined ,同时只要任一的 reducer 改变了 state,hasChanged 的值都为true,都将返回新的 state。

   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]
     const nextStateForKey = reducer(previousStateForKey, action)
     if (typeof nextStateForKey === 'undefined') {
       const errorMessage = getUndefinedStateErrorMessage(key, action)
       throw new Error(errorMessage)
     }
     nextState[key] = nextStateForKey
     hasChanged = hasChanged || nextStateForKey !== previousStateForKey
   }
   return hasChanged ? nextState : state

其实核心逻辑的代码量可能还不如判空,判 undefined ,输出警告等行为的代码量,但是一个优秀的插件,对不合理的值应该有合适的处理而不是等着运行环境给我们报错,这是很多新手前端(比如我)会忽视的内容,日常可能还得靠各种报错一点点累计经验...以至于不会重视报错,认为有错改一下就好了。

bindActionCreators.js

这个函数我们经常用...但你可能并不太了解他的作用:

const mapStateToProps = state => {
  return {
    stateKey: state.stateKey
  };
};
const mapDispatchToProps = dispatch =>
  bindActionCreators(
    {
      oneAction
    },
    dispatch
  );

export default connect(mapStateToProps, mapDispatchToProps)(Component);

以前的认知就是执行了这些,就可以在组件下直接 this.props.oneActionCreator(actionParam)来发起一个 action,而不是 this.store.dispatch(oneActionCreator(actionParam))

我们先从源码的返回值 开始看,如下图。


image.png

函数返回 boundActionCreators 我们再追溯的看这个变量如何赋值的。

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

可以大致得知,函数主要做了这样的操作。


图左是actionCreators,图右是返回值

那关键步骤就是bindActionCreator 了,而其也十分简单...

function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

朋友们还记得 dispatch 做了什么吗~ 分发action!没错,bindActionCreator直接帮你在创建 action 时 dispatch 这个 action。
综述,bindActionCreators.js 的处理过程如下图。

image.png

可是这里为什么要使用 apply 呢?为什么不直接 return dispatch(actionCreator(arguments)) ?我们知道以函数形式调用的函数的 this 的值是全局对象 ( javascript 权威指南第八章),使用 apply 后间接等于方法调用,这里形如 this. actionCreator(arguments),方法形式调用的函数其上下文 (this) 为调用的对象。所以这里的 this 是打算将调用 bindActionCreator 内部函数的上下文传递给 actionCreator 使用。

applyMiddleware.js

image.png

该模块返回函数 applyMiddleware ,参数 ...middlewares 类似于用 arguments 来接受一系列参数。函数内部返回值仍然是函数。注意 返回的值 是用于 createStore 的第三个参数 enhancer。

return createStore => (...args) => {
    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.`
      )
    }
    let chain = []

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }

箭头函数真的不太适合阅读...(可能是我不太习惯)

return function (createStore) {
  return function (...args) {
    const store = createStore(...args)
    ...
  }
}

符合 enhancer 在 createStore 的逻辑enhancer(createStore)(reducer, preloadedState)
观察函数的返回值,可以得知函数内部只改动了 dispatch 方法,再看源码。

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

compose 方法是另一个文件导入的,源码也不多,比较关键的一句是 return funcs.reduce((a, b) => (...args) => a(b(...args))),但是这一句让不熟悉函数式编程的我思考了很久......

compose(...chain),输出这一句的执行结果

function (...args){
  return a( b(...args) )
}

看起来只是个普通的函数,只是这里的 a ,b 是谁呢?ಥ_ಥ
我们知道 reduce 返回值是最后一次累加计算的结果,compose(...chain) 的返回值仍然是个函数,函数内的 a 就是上一次累加返回的结果,b 是当前项,也就是 chain 的最后一项;上一次累加结果仍然是 这个普通的函数,只不是此时的 b 是 chain 的倒数第二项,a 是再上一次的累加返回结果...

dispatch = compose(...chain)(store.dispatch)
此时再看这一句,其实就是:先以参数,运行了 chain 的最后一项函数,返回值做为参数被 a 调用,a是上一次的返回值,再次跳回 2 行,此时的 b 是 chain 的倒数第二项...以此类推,到chain 的第二项时(reduce 没有 设置初始值时,accumulator 的默认值为调用数组的第一项,同时从第二项开始累加计算),以 chain 第三项运行的返回值做为参数,调用 第二项,其后的返回值,做为参数 调用 chain 的第一项,然后 执行 3行,再次跳回3 行 ... 返回值 赋给 dispatch。

  // funcs 是该函数的参数数组。
  return funcs.reduce((a, b) => (...args) => a(b(...args)))

//可以转换成
0  return  funcs.reduce( function (a,b) {
1     return function (...args){
2       return a( b(...args) )
3   }
4  })

总结下来就是...
假设数组 chain 是 [ fun1, fun2, fun3],相当于最后以
fun1(fun2(fun3(store.dispatch)))这样的形式调用了。

写到这里时我并不是十分了解中间件的作用,不过从此处的代码可以推断,中间件需要接受一个形如 middlewareAPI 的参数,且返回值仍然是函数,返回值很有可能仍然是 store.dispatch...

    let chain = []

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))

也就是说中间件主要对 store.dispatch 进行一些改造,同时也拿了 getState 方法做一些事情。


image.png

此时在看 applyMiddleware 的用法,至少不会觉得茫然吧...(大概 ಠ౪ಠ)

const enhancer = applyMiddleware(
  thunk,
  createLogger()
);

const store = createStore(rootReducer, initialState, enhancer);

初学 redux 我觉得最困难的就是记很多名词,很多规矩,redux 规定了很多东西,让我摸不着头脑。为此我送上一份源码脉络梳理图。
最后送上一份源码脉络梳理图,画的有些粗略... 本意就是希望看到图还能回忆起各个函数大概做了些什么事情...

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

推荐阅读更多精彩内容