Redux源码分析

Redux使用中的几个点:javascript

  1. Redux三大设计原则
  2. Create Store
  3. Redux middleware
  4. combineReducer
  5. Provider与Connect
  6. Redux流程梳理
  7. Redux设计特色

1. Redux三大设计原则

1. 单一数据源

在传统的 MVC 架构中,咱们能够根据须要建立无数个 Model,而 Model 之间能够互相监听、触发事件甚至循环或嵌套触发事件,这些在 Redux 中都是不容许的。由于在 Redux 的思想里,一个应用永远只有惟一的数据源。
实际上,使用单一数据源的好处在于整个应用状态都保存在一个对象中,这样咱们随时能够提取出整个应用的状态进行持久化(好比实现一个针对整个应用的即时保存功能)。此外,这样的设计也为服务端渲染提供了可能。java

2. 状态是只读的

在 Redux 中,咱们并不会本身用代码来定义一个 store。取而代之的是,咱们定义一个 reducer,它的功能是根据当前触发的 action 对当前应用的状态(state)进行迭代,这里咱们并无直接修改应用的状态,而是返回了一份全新的状态。react

Redux 提供的 createStore 方法会根据 reducer 生成 store。最后,咱们能够利用 store. dispatch
方法来达到修改状态的目的。编程

3.状态修改均由纯函数完成

在 Redux 里,咱们经过定义 reducer 来肯定状态的修改,而每个 reducer 都是纯函数,这意味着它没有反作用,即接受必定的输入,一定会获得必定的输出。redux

这样设计的好处不只在于 reducer 里对状态的修改变得简单、纯粹、可测试,更有意思的是,Redux 利用每次新返回的状态生成酷炫的时间旅行(time travel)调试方式,让跟踪每一次由于触发 action 而改变状态的结果成为了可能。api

2.Create Store

咱们从store的诞生开始提及。create store函数API文档以下:数组

createStore(reducer, [initialState], enhancer)

能够看出,它接受三个参数:reducer、initialState 和 enhancer 。Store enhancer 是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。这与 middleware 类似,它也容许你经过复合函数改变 store 接口。缓存

再来看看他的返回值:闭包

{
    dispatch: f (action),
    getState: f (),
    replaceReducer: f (nextReducer),
    subscribe: f (listener),
    Symbol(observable): f ()    
}

store的返回值就是一个普通对象,里面有几个经常使用的方法:架构

  • dispatch:就是咱们最经常使用的dispatch方法,派发action。
  • getState:经过该方法,咱们能够拿到当前状态树state。
  • replaceReducer:这个方法主要用于 reducer 的热替换,下面介绍该方法。
  • subscribe:添加一个变化监听器。每当 dispatch(action)的时候就会执行,state 树中的一部分可能已经变化。
  • observable:观察者模式,用于处理订阅关系。

这里挑几个方法介绍:

getState

在完成基本的参数校验以后,在 createStore 中声明以下变量及 getState 方法:

var currentReducer = reducer
var currentState = initialState
var listeners = [] // 当前监听 store 变化的监听器
var isDispatching = false // 某个 action 是否处于分发的处理过程当中
/**
* Reads the state tree managed by the store.
 *
* @returns {any} The current state tree of your application.
 */
function getState() {
 return currentState
}

getState方法就是简单返回当前state,若是state没有被reducer处理过,他就是initialState。

subscribe

在 getState 以后,定义了 store 的另外一个方法 subscribe:

function subscribe(listener) {
 listeners.push(listener)
 var isSubscribed = true
 return function unsubscribe() {
 if (!isSubscribed) {
 return
 }
 isSubscribed = false
 var index = listeners.indexOf(listener)
 listeners.splice(index, 1)
 }
}

Store 容许使用store.subscribe方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。

显然,只要把 View 的更新函数(对于 React 项目,就是组件的render方法或setState方法)放入listen,就会实现 View 的自动渲染。你可能会感到奇怪,好像咱们在 Redux 应用中并无使用 store.subscribe 方法?事实上,

React Redux 中的 connect 方法隐式地帮咱们完成了这个工做。

store.subscribe方法返回一个函数,调用这个函数就能够解除监听。

dispatch

dispatch是redux的核心方法:

function dispatch(action) {
    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?'
            )
        }
    if (isDispatching) {
        throw new Error('Reducers may not dispatch actions.')
    }
    try {
        isDispatching = true
        currentState = currentReducer(currentState, action)
    } finally {
        isDispatching = false
    }
    listeners.slice().forEach(listener => listener())
    return action
}

判断当前是否处于某个 action 的分发过程当中,这个检查主要是为了不在 reducer 中分发 action 的状况,由于这样作可能致使分发死循环,同时也增长了数据流动的复杂度。

确认当前不属于分发过程当中后,先设定标志位,而后将当前的状态和 action 传给当前的reducer,用于生成最新的 state。这看起来一点都不复杂,这也是咱们反复强调的 reducer 工做过程——纯函数、接受状态和 action 做为参数,返回一个新的状态。

在获得新的状态后,依次调用全部的监听器,通知状态的变动。须要注意的是,咱们在通知监听器变动发生时,并无将最新的状态做为参数传递给这些监听器。这是由于在监听器中,咱们能够直接调用 store.getState() 方法拿到最新的状态。

最终,处理以后的 action 会被 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 });
  }

这是为了拿到全部 reducer 中的初始状态(你是否还记得在定义 reducer 时,第一个参数为previousState,若是该参数为空,咱们提供默认的 initialState)。只有全部的初始状态都成功获取后,Redux 应用才能有条不紊地开始运做。

3.Redux middleware

It provides a third-party extension point between dispatching an action, and the moment it reaches
the reducer

它提供了一个分类处理 action 的机会。在middleware 中,你能够检阅每个流过的 action,挑选出特定类型的action 进行相应操做,给你一次改变 action 的机会。

常规的同步数据流模式的流程图以下:
clipboard.png
不一样业务需求下,好比执行action以前和以后都要打log;action触发一个异步的请求,请求回来以后渲染view等。须要为这一类的action添加公共的方法或者处理,使用redux middleware流程图以下:
clipboard.png
每个 middleware 处理一个相对独立的业务需求,经过串联不一样的 middleware 实现变化多样的功能。好比上面的业务,咱们把处理log的代码封装成一个middleware,处理异步的也是一个middleware,二者串联,却又相互独立。

使用middleware以后,action触发的dispatch并非原来的dispatch,而是通过封装的new dispatch,在这个new dispatch中,按照顺序依次执行每一个middleware,最后调用原生的dispatch。

咱们来看下logger middleware如何实现的:

export default store => next => action => {
    console.log('dispatch:', action); 
    next(action);
    console.log('finish:', action);
 }

这里代码十分简洁,就是在next调用下一个middleware以前和以后,分别打印两次。

Redux 提供了 applyMiddleware 方法来加载 middleware,该方法的源码以下:

import compose from './compose';

export default function applyMiddleware(...middlewares) {
    return function (next) {
        return function (reducer, initialState) {
            let store = next(reducer, initialState);
            let dispatch = store.dispatch;
            let chain = [];
            var middlewareAPI = {
                getState: store.getState,
                dispatch: (action) => dispatch(action),
            };
            chain = middlewares.map(middleware => middleware(middlewareAPI));
            dispatch = compose(...chain)(store.dispatch);

            return {
                ...store,
                dispatch,
            };
        }
    }
}

其中compose源码以下:

function compose(...funcs) {
    return arg => funcs.reduceRight((composed, f) => f(composed), arg);
}

使用的时候,以下:

const newStore = applyMiddleware([mid1, mid2, mid3, ...])(createStore)(reducer, initialState);

ok,相关源码已就位,咱们来详细解析一波。

函数式编程思想设计 :middleware 的设计有点特殊,是一个层层包裹的匿名函数,这实际上是函数式编程中的
currying,它是一种使用匿名单参数函数来实现多参数函数的方法。applyMiddleware 会对 logger 这个middleware 进行层层调用,动态地将 store 和 next 参数赋值。currying 的 middleware 结构的好处主要有如下两点。

  • 易串联:currying 函数具备延迟执行的特性,经过不断 currying 造成的 middleware 能够累积参数,再配合组合(compose)的方式,很容易造成 pipeline 来处理数据流。
  •  共享 store: 在 applyMiddleware 执行的过程当中,store 仍是旧的,可是由于闭包的存在,applyMiddleware 完成后,全部的 middleware 内部拿到的 store 是最新且相同的。

给 middleware 分发 store:newStore建立完成以后,applyMiddleware 方法陆续得到了3个参数,第一个是 middlewares 数组[mid1, mid2, mid3, ...],第二个是 Redux 原生的 createStore ,最后一个是 reducer。而后,咱们能够看到 applyMiddleware 利用 createStore 和 reducer 建立了一个 store。而 store 的 getState方法和 dispatch 方法又分别被直接和间接地赋值给 middlewareAPI 变量 store:

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

而后,让每一个 middleware 带着 middlewareAPI 这个参数分别执行一遍。执行完后,得到 chain数组 [f1, f2, ... , fx, ..., fn],它保存的对象是第二个箭头函数返回的匿名函数。由于是闭包,每一个匿名函数均可以访问相同的 store,即 middlewareAPI。

middlewareAPI 中的 dispatch 为何要用匿名函数包裹呢?

咱们用 applyMiddleware 是为了改造 dispatch,因此 applyMiddleware 执行完后,dispatch 是变化了的,而 middlewareAPI 是 applyMiddleware 执行中分发到各个 middleware 的,因此必须用匿名函数包裹 dispatch,这样只要 dispatch 更新了,middlewareAPI 中的 dispatch 应用也会发生变化。

组合串联 middleware:这一层只有一行代码,倒是 applyMiddleware 精华之所在dispatch = compose(...chain)(store.dispatch); ,其中 compose 是函数式编程中的组合,它将 chain 中的全部匿名函数 [f1, f2, ... , fx, ..., fn]组装成一个新的函数,即新的 dispatch。当新 dispatch 执行时,[f1, f2, ... , fx, ..., fn],从右到左依次执行。

compose(...funcs) 返回的是一个匿名函数,其中 funcs 就是 chain 数组。当调用 reduceRight时,依次从 funcs 数组的右端取一个函数 fx 拿来执行,fx 的参数 composed 就是前一次 fx+1 执行的结果,而第一次执行的 fn(n 表明 chain 的长度)的参数 arg 就是 store.dispatch。因此,当 compose 执行完后,咱们获得的 dispatch 是这样的,假设 n = 3:

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

这时调用新 dispatch,每个 middleware 就依次执行了。

在 middleware 中调用 dispatch 会发生什么:通过 compose 后,全部的 middleware 算是串联起来了。但是还有一个问题,在分发 store 时,咱们提到过每一个 middleware 均可以访问 store,即 middlewareAPI 这个变量,也能够拿到 store 的dispatch 属性。那么,在 middleware 中调用 store.dispatch() 会发生什么,和调用 next() 有区别吗?如今咱们来讲明二者的不一样:

const logger = store => next => action => {
 console.log('dispatch:', action);
 next(action);
 console.log('finish:', action);
};
const logger = store => next => action => {
 console.log('dispatch:', action);
 store.dispatch(action);
 console.log('finish:', action);
};

在分发 store 时咱们解释过,middleware 中 store 的 dispatch 经过匿名函数的方式和最终compose 结束后的新 dispatch 保持一致,因此,在 middleware 中调用 store.dispatch() 和在其余任何地方调用的效果同样。而在 middleware 中调用 next(),效果是进入下一个 middleware,下图就是redux middleware最著名的洋葱模型图。
clipboard.png

4.combineReducer

若是一个项目过大,咱们一般按模块来写reducer,可是redux create store只接受一个reducer参数,因此咱们须要合并reducer。这里就用到了redux提供的combineReducer辅助函数:

combineReducers({
      layout,
      home,
      ...asyncReducers
  })

这个函数用起来很简单,就是传入一个对象,key是模块reducer对应的名字, 值是对应reducer。值是一个function,至关因而一个新的reducer,源码以下:

export default function combineReducers(reducers) {
  var reducerKeys = Object.keys(reducers)
  var finalReducers = {}
  for (var i = 0; i < reducerKeys.length; i++) {
    var 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]
    }
  }
  var finalReducerKeys = Object.keys(finalReducers)

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

  var sanityError
  try {
    assertReducerSanity(finalReducers)
  } catch (e) {
    sanityError = e
  }

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

    if (process.env.NODE_ENV !== 'production') {
      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache)
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    var hasChanged = false
    var nextState = {}
    for (var i = 0; i < finalReducerKeys.length; i++) {
      var key = finalReducerKeys[i]
      var reducer = finalReducers[key]
      var previousStateForKey = state[key]
      var nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        var errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}

源码不是不少,除去一些验证代码,剩下的就是说:return一个function,咱们暂时称呼他combination,就至关因而与一个总的reducer,每次action都会走到combination中,combination会遍历输入的reducer,将action放到每一个reducer中执行一下,计算出返回结果就是nextState,nextState于previousState若是!==说明改变了,返回nextState,不然返回执行以前的state。

这也解释了不一样模块actionType若是相同的话,两个模块的reducer都会走一遍的问题,在actionType名称前面加上模块前缀便可解决问题。

5. Provider与Connect

Provider与Connet组件都是React-Redux提供的核心组件,二者看起来功能同样,都是帮助容器组件获取store中的数据,可是原理与功能却不一样。

Provider

Provider组件在全部组件的最外层,其接受store做为参数,将store里的state使用context属性向下传递。部分源码:

export default class Provider extends Component {
 getChildContext() {
 return { store: this.store }
 }
 constructor(props, context) {
 super(props, context)
 this.store = props.store
 }
 render() {
 const { children } = this.props
 return Children.only(children)
 }
}

利用context这个属性,Provider全部子组件都可以拿到这个属性。

Connect

connect实现的功能是将须要关联store的组件和store的dispatch等数据混合到一块,这块就是一个高阶组件典型的应用:

import hoistStatics from 'hoist-non-react-statics'
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
 // ...
 return function wrapWithConnect(WrappedComponent) {
 // ...
 class Connect extends Component {
 // ...
 render() {
 // ...
 if (withRef) {
 this.renderedElement = createElement(WrappedComponent, {
 ...this.mergedProps,
 ref: 'wrappedInstance'
 })
 } else {
 this.renderedElement = createElement(WrappedComponent,
 this.mergedProps
 )
 }
 return this.renderedElement
 }
 }
 // ...
 return hoistStatcis(Connect, WrappedComponent);
 }
}

仍是先从他的四个参数提及:

1.mapStateToProps

connect 的第一个参数定义了咱们须要从 Redux 状态树中提取哪些部分看成 props 传给当前组件。通常来讲,这也是咱们使用 connect 时常常传入的参数。事实上,若是不传入这个参数,React 组件将永远不会和 Redux 的状态树产生任何关系。具体在源代码中的表现为:

export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
 const shouldSubscribe = Boolean(mapStateToProps)
 // ...
 class Connect extends Component {
 // ...
 trySubscribe() {
 if (shouldSubscribe && !this.unsubscribe) {
 this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))
 this.handleChange()
 }
 }
 // ...
 }
}

mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,从新计算 UI 组件的参数,从而触发 UI 组件的从新渲染。

mapStateToProps的第一个参数老是state对象,还可使用第二个参数,表明容器组件的props对象。

这块的源码相对较简单:

const mapState = mapStateToProps || defaultMapStateToProps 
class Connect extends Component { 
    computeStateProps(store, props) {
        if (!this.finalMapStateToProps) {
          return this.configureFinalMapState(store, props)
        }

        const state = store.getState()
        const stateProps = this.doStatePropsDependOnOwnProps ?
          this.finalMapStateToProps(state, props) :
          this.finalMapStateToProps(state)

        if (process.env.NODE_ENV !== 'production') {
          checkStateShape(stateProps, 'mapStateToProps')
        }
        return stateProps
      }

      configureFinalMapState(store, props) {
        const mappedState = mapState(store.getState(), props)
        const isFactory = typeof mappedState === 'function'

        this.finalMapStateToProps = isFactory ? mappedState : mapState
        this.doStatePropsDependOnOwnProps = this.finalMapStateToProps.length !== 1

        if (isFactory) {
          return this.computeStateProps(store, props)
        }

        if (process.env.NODE_ENV !== 'production') {
          checkStateShape(mappedState, 'mapStateToProps')
        }
        return mappedState
      }
}

这块原理很简单,进行一些参数校验,判断第一个参数mapStateToProps返回值是否为function,若是是递归调用,不是的话算出返回值。若是没传这个参数,默认给{}。

咱们可能会疑惑为何传给 connect 的第一个参数自己是一个函数,react-redux 还容许这个函数的返回值也是一个函数呢?
简单地说,这样设计能够容许咱们在 connect 的第一个参数里利用函数闭包进行一些复杂计算的缓存,从而实现效率优化的目的

当咱们使用的时候:

const mapStateToProps = (state, props) => ({
    home: state.home,
    layout: state.layout
});

使用ownProps做为参数后,若是容器组件的参数发生变化,也会引起 UI 组件从新渲染

2.mapDispatchToProps

人如其名,它接受 store 的 dispatch 做为第一个参数,同时接受 this.props 做为可选的第二个参数。利用这个方法,咱们能够在 connect 中方便地将 actionCreator 与 dispatch 绑定在一块儿(利用 bindActionCreators 方法),最终绑定好的方法也会做为 props 传给当前组件。这块的源码与mapStateToProps同样,就不贴了。

bindActionCreator

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}

3.mergeProps

前两个参数返回的对象,都要跟组件自身的props merge一下,造成一个新的对象赋值给对应组件,咱们能够在这一步作一些处理,这个参数就是干这个的,该参数签名:

mergeProps(stateProps, dispatchProps, ownProps): props

默认状况若是没传该参数,返回Object.assign(ownProps, stateProps, dispatchProps)

4.options

若是指定这个参数,能够定制 connector 的行为。

  • [pure = true] (Boolean): 若是为 true,connector 将执行 shouldComponentUpdate 而且浅对比 mergeProps 的结果,避免没必要要的更新,前提是当前组件是一个“纯”组件,它不依赖于任何的输入或 state 而只依赖于 props 和 Redux store 的 state。默认值为 true。
  • [withRef = false] (Boolean): 若是为 true,connector 会保存一个对被包装组件实例的引用,该引用经过 getWrappedInstance() 方法得到。默认值为 false。

这个connect组件还干了一件事,状态缓存判断。当store变了的时候,先后状态判断,若是状态不等,更新组件,而且完成事件分发。

6. Redux流程梳理

上面讲了大量的函数源码,这么些函数之间的关系:
clipboard.png
初始化阶段:

  1. createStore建立一个store对象
  2. 将store对象经过参数给Provider组件
  3. Provider组件将store经过context向子组件传递
  4. Connect组件经过context获取到store,存入本身的state
  5. componentDidMount里面订阅store.subscribe事件

更新数据阶段:

  1. 用户事件触发
  2. actionCreator生成action交给dispatch
  3. 实际上交给了封装后的中间层(compose(applyMiddleware(...)))
  4. 请求依次经过每一个中间件,中间件经过next进行下一步
  5. 最后一个中间件将action交给store.dispatch
  6. dispatch内部将action交给reducer执行
  7. combineReducer将每一个子reducer执行一遍算出新的state
  8. dispatch内部调用全部订阅事件
  9. Connect组件handleChange事件触发判断新state和旧state是否===
  10. 而且判断新的state是否与mapStateToProps shallowEqual
  11. 不等则setState触发更新

7.Redux设计技巧

  1. 匿名函数&&闭包使用

    redux核心函数大量使用了匿名函数和闭包来实现数据共享和状态同步。

  2. 函数柯里化使用

    使用函数柯里化s实现参数复用,本质上是下降通用性,提升适用性。

  3. 核心状态读取是拷贝而不是地址

    对于state这种核心状态使用getState()计算出新的state,而不是直接返回一个state对象。

  4. 观察者订阅者是核心实现

    使用观察者订阅者模式实现数据响应。

  5. context这个api的使用

    平时开发不常接触的api实现Provider与Connect通讯。

相关文章
相关标签/搜索