浅析Redux源码

@(Redux)[|用法|源码]

Redux 由Dan Abramov在2015年创建的科技术语。是受2014年Facebook的Flux架构以及函数式编程语言Elm启发。很快,Redux因其简单易学体积小短时间内成为最热门的前端架构。

@[三大原则]

  • 单一数据源 - 整个应用的state被储存在一棵object tree中,并且这个object tree只存在于唯一一个store中。所有数据会通过store.getState()方法调用获取.
  • **State‘只读’ ** - 根据State只读原则,数据变更会通过store,dispatch(action)方法.
  • 使用纯函数修改 -Reducer只是一些纯函数[1],它接收先前的stateaction,并返回新的state.

[TOC]

准备阶段

柯里化函数(curry)

    //curry example
    const A  = (a) => {
        return (b) => {
            return a + b
        }
    }

通俗的来讲,可以用一句话概括柯里化函数:返回函数的函数.
优点: 避免了给一个函数传入大量的参数,将参数的代入分离开,更有利于调试。降低耦合度和代码冗余,便于复用.

代码组合(compose)

举个例子

    let init = (...args) => args.reduce((ele1, ele2) => ele1 + ele2, 0)
    let step2 = (val) => val + 2
    let step3 = (val) => val + 3
    let step4 = (val) => val + 4
    let steps = [step4, step3, step2, init]
    let composeFunc = compose(...steps)
    console.log(composeFunc(1, 2, 3))
    // 1+2+3+2+3+4 = 15

接下来看下FP思想的compose的源码

    const compose = function (...args) {
      let length = args.length
      let count = length - 1
      let result
      let this_ = this
      // 递归
      return function f1(...arg1) {
        result = args[count].apply(this, arg1)
        if (count <= 0) {
          count = length - 1
          return result
        }
        count--
        return f1.call(null, result)
      }
    }

通俗的讲: 从右到左执行函数,最右函数以arguments为参数,其余函数以上个函数结果为入参数执行。

优点: 通过这样函数之间的组合,可以大大增加可读性,效果远大于嵌套一大堆的函数调用,并且我们可以随意更改函数的调用顺序

CombineReducers

作用

随着整个项目越来越大,state状态树也会越来越庞大,state的层级也会越来越深,由于redux只维护唯一的state,当某个action.type所对应的需要修改state.a.b.c.d.e.f时,我的函数写起来就非常复杂,我必须在这个函数的头部验证state 对象有没有那个属性。这是让开发者非常头疼的一件事。于是有了CombineReducers。我们除去源码校验函数部分,从最终返回的大的Reducers来看。

Note:

  • FinalReducers : 通过=== 'function'校验后的Reducers.
  • FinalReducerKeys : FinalReducers的所有key
    (与入参Objectkey区别:过滤了value不为function的值)

源码

      // 返回一个function。该方法接收state和action作为参数
      return function combination(state = {}, action) {
        var hasChanged = false
        var nextState = {}
        // 遍历所有的key和reducer,分别将reducer对应的key所代表的state,代入到reducer中进行函数调用
        for (var i = 0; i < finalReducerKeys.length; i++) {
          var key = finalReducerKeys[i]
          var reducer = finalReducers[key]
          // CombineReducers入参Object中的Value为reducer function,从这可以看出reducer function的name就是返回给store中的state的key。
          var previousStateForKey = state[key]
          // debugger
          var nextStateForKey = reducer(previousStateForKey, action)
          // 如果reducer返回undefined则抛出错误
          if (typeof nextStateForKey === 'undefined') {
            var errorMessage = getUndefinedStateErrorMessage(key, action)
            throw new Error(errorMessage)
          }
          // 将reducer返回的值填入nextState
          nextState[key] = nextStateForKey
          // 如果任一state有更新则hasChanged为true
          hasChanged = hasChanged || nextStateForKey !== previousStateForKey
        }
        return hasChanged ? nextState : state
      }

小结

combineReducers实现方法很简单,它遍历传入的reducers,返回一个新的reducer.该函数根据Statekey 去执行相应的子Reducer,并将返回结果合并成一个大的State 对象。

CreateStore

作用

createStore主要用于Store的生成,我们先整理看下createStore具体做了哪些事儿。(这里我们看简化版代码)

源码(简化版)

const createStore = (reducer, initialState) => {
      // initialState一般设置为null,或者由服务端给默认值。
      // internal variables
      const store = {};
      store.state = initialState;
      store.listeners = [];
      // api-subscribe
      store.subscribe = (listener) => {
        store.listeners.push(listener);
      };
      // api-dispatch
      store.dispatch = (action) => {
        store.state = reducer(store.state, action);
        store.listeners.forEach(listener => listener());
      };
      // api-getState
      store.getState = () => store.state;
      
      return store;
    }

小结

源码角度,一大堆类型判断先忽略,可以看到声明了一系列函数,然后执行了dispatch方法,最后暴露了dispatchsubscribe……几个方法。这里dispatch了一个init Action是为了生成初始的State树。

ThunkMiddleware

作用

首先,说ThunkMiddleware之前,也许有人会问,到底middleware有什么用?
这就要从action说起。在redux里,action仅仅是携带了数据的普通js对象。action creator返回的值是这个action类型的对象。然后通过store.dispatch()进行分发……

action ---> dispatcher ---> reducers
同步的情况下一切都很完美……
如果遇到异步情况,比如点击一个按钮,希望1秒之后显示。我们可能这么写:

function (dispatch) {
        setTimeout(function () {
            dispatch({
                type: 'show'
            })
        }, 1000)
    }

这会报错,返回的不是一个action,而是一个function。这个返回值无法被reducer识别。

大家可能会想到,这时候需要在actionreducer之间架起一座桥梁……
当然这座桥梁就是middleware。接下来我们先看看最简单,最精髓的ThunkMiddleware的源码

源码

const thunkMiddleware = ({ dispatch, getState }) => {
      return next => action => {
        typeof action === 'function' ?
          action(dispatch, getState) :
          next(action)
      }
    }

非常之精髓。。。我们先记住上述代码,引出下面的ApplyMiddleware

ApplyMiddleware

作用

介绍applyMiddleware之前我们先看下项目中store的使用方法如下:

  let step = [ReduxThunk, middleware, ReduxLogger]
  let store = applyMiddleware(...step)(createStore)(reducer)
  return store

通过使用方法可以看到有3处柯里化函数的调用,applyMiddleware 函数Redux 最精髓的地方,成功的让Redux 有了极大的可拓展空间,在action 传递的过程中带来无数的“副作用”,虽然这往往也是麻烦所在。 这个middleware的洋葱模型思想是从koa的中间件拿过来的,用图来表示最直观。

洋葱模型

洋葱模型.png

我们来看源码:

源码

    const applyMiddleware = (...middlewares) => {
      return (createStore) => (reducer, initialState, enhancer) => {
        var store = createStore(reducer, initialState, enhancer)
        var dispatch
        var chain = []
        var middlewareAPI = {
          getState: store.getState,
          dispatch: (action) => dispatch(action)
        }
        // 每个 middleware 都以 middlewareAPI 作为参数进行注入,返回一个新的链。
        // 此时的返回值相当于调用 thunkMiddleware 返回的函数: (next) => (action) => {} ,接收一个next作为其参数
        chain = middlewares.map(middleware => middleware(middlewareAPI))
        // 并将链代入进 compose 组成一个函数的调用链
        dispatch = compose(...chain)(store.dispatch)
        return {
          ...store,
          dispatch
        }
      }
    }

applyMiddleware函数第一次调用的时候,返回一个以createStore为参数的匿名函数,这个函数返回另一个以reducer,initialState,enhancer为参数的匿名函数.我们在使用方法中,分别可以看到传入的值。
结合一个简单的实例来理解中间件以及洋葱模型

    // 传入middlewareA
    const middlewareA = ({ dispatch, getState }) => {
      return next => action => {
        console.warn('A middleware start')
        next(action)
        console.warn('A middleware end')
      }
    }
    // 传入多个middlewareB
    const middlewareB = ({ dispatch, getState }) => {
      return next => action => {
        console.warn('B middleware start')
        next(action)
        console.warn('B middleware end')
      }
    }
    // 传入多个middlewareC
    const middlewareC = ({ dispatch, getState }) => {
      return next => action => {
        console.warn('C middleware start')
        next(action)
        console.warn('C middleware end')
      }
    }

当我们传入多个类似A,B,C的middlewareapplyMiddleware后,调用

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

结合场景并且执行compose结果为:

dispatch = middlewareA(middlewareB(middlewareC(store.dispatch)))

从中我们可以清晰的看到middleware函数中的next函数相互连接,这里体现了compose FP编程思想中代码组合的强大作用。再结合洋葱模型的图片,不难理解是怎么样的一个工作流程。

最后我们看结果,当我们触发一个store.dispath的时候进行分发。则会先进入middlewareA并且打印A start然后进入next函数,也就是middlewareB同时打印B start,然后触发next函数,这里的next函数就是middlewareC,然后打印C start,之后才处理dispath,处理完成后先打印C end,然后B end,最后A end。完成整体流程。

小结

  • Redux applyMiddleware机制的核心在于,函数式编程(FP)compose组合函数,需将所有的中间件串联起来。
  • 为了配合compose对单参函数的使用,对每个中间件采用currying的设计。同时,利用闭包原理做到每个中间件共享Store。(middlewareAPI的注入)

Feedback & Bug Report


Thank you for reading this record.


  1. 纯函数,它不依赖于外部环境(例如:全局变量、环境变量)、不改变外部环境(例如:发送请求、改变DOM结构),函数的输出完全由函数的输入决定。 比如 slice 和 splice,这两个函数的作用并无二致——但是注意,它们各自的方式却大不同,但不管怎么说作用还是一样的。我们说 slice 符合纯函数的定义是因为对相同的输入它保证能返回相同的输出。而 splice 却会嚼烂调用它的那个数组,然后再吐出来;这就会产生可观察到的副作用,即这个数组永久地改变了。可以看到,splice改变了原始数组,而slice没有。我们认为,slice不改变原来数组的方式更加“安全”。改变原始组数,是一种“副作用”。.

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

推荐阅读更多精彩内容

  • http://gaearon.github.io/redux/index.html ,文档在 http://rac...
    jacobbubu阅读 79,888评论 35 198
  • 本文将开始详细分析如何搭建一个React应用架构。 一. 前言 现在已经有很多脚手架工具,如create-reac...
    字节跳动技术团队阅读 4,274评论 1 23
  • 学习必备要点: 首先弄明白,Redux在使用React开发应用时,起到什么作用——状态集中管理 弄清楚Redux是...
    贺贺v5阅读 8,877评论 9 58
  • 前言 本文 有配套视频,可以酌情观看。 文中内容因各人理解不同,可能会有所偏差,欢迎朋友们联系我讨论。 文中所有内...
    珍此良辰阅读 11,892评论 23 111
  • 为什么dispatch需要middleware 上图表达的是 redux 中一个简单的同步数据流动的场景,点击 b...
    一个胖子的我阅读 1,977评论 1 9