Redux源码分析

1 redux使用步骤

React仅仅是一个前端View框架库,可以看做是MVC里面的V。没有解决组件间通信,MVC分离,数据共享等问题。Redux的出现使得这些都不是问题。使用Redux也比较简单,步骤大概如下:
1.编写React Component,这里不涉及Redux
2.编写reducer,它接收一个state和action,返回一个新的state
3.createStore(reducer) 得到store对象,它是Redux的核心对象。包含的主要API如下 :
(1).getState(), 得到store中保存的state对象
(2).dispatch(action), 发送action消息给store,它会调用reducer更新state,并回调subscribe的监听器
(3).subcribe(listener), 注册监听器,dispatch触发时会回调
4.编写React Component中的事件回调,比如onClick。使用store.dispatch(action)发送消息,reducer此时会得到调用,更新state。

其中createStore(reducer)比较关键,下面来分析它的源码

2 createStore()源码分析

function createStore(reducer, preloadedState, enhancer) {
  // reducer, preloadedState, enhancer 入参检查 

  let currentReducer = reducer
  let currentState = preloadedState  // createStore传入的state会存储为初始state
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  // 判断是否能触发nextListeners
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // 返回store中存储的state
  function getState() {
    return currentState
  }

  // 注册事件,加入到listener数组中。每次dispatch调用时,会回调注册的listener
  function subscribe(listener) {
    let isSubscribed = true

    // 存储listener
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    // 取消订阅事件
    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      // 找到要取消订阅的listener,然后从数组中删除,以后就不会回调到它了
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

  // 发送消息action。主要两件事
  // 1. 调用reducer更新state
  // 2. 回调subscribe注册的listeners
  function dispatch(action) {
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      // 调用reducer来更新state, 传入当前state和action
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    // 回调subscribe的listeners, dispatch会触发回调listener
    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

  // createStore时触发一次dispatch, 一般会调用到我们reducer里面的default分支,此时返回默认state,更新currentState
  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
  }

源码分析中对一些不是很关键的地方做了省略,我们重点关注dispatch, subscribe, getState三个API即可。其中dispatch稍微麻烦点,它主要做两件事

调用reducer更新state
回调subscribe注册的listeners

3 React-redux 使用步骤

Redux已经足够优秀了,但需要用户手动调用setState来更新view,也没有建立store上的state数据和React Component之间联系,而React-redux框架很好的解决了这个问题。它的使用步骤如下
1.编写UI组件,也就是React Component。这部分和Redux无关
2.编写mapStateToProps, mapDispatchToProps,然后connect(mapStateToProps, mapDispatchToProps)(UI组件)来生成容器组件。容器组件包含了UI,数据和逻辑。

3.编写reducers,完成控制逻辑。这部分和只是用Redux相同

4.应用顶层做整合
(1).createStore(reducers) 生成store
(2).使用Provider包装顶层组件,如下

ReactDOM.render(
<Provider store={store}>
  <App />
</Provider>,
document.getElementById('root')
);

步骤不复杂,下面我们来分析下connect源码

4 connect源码分析

connectAPI为connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]), 后两个参数不是很关键,我们源码分析中省略它们,这样以最简洁的方式分析主流程

function connect(mapStateToProps, mapDispatchToProps) {
  const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories, 'mapStateToProps')
  const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps')

  return connectHOC(selectorFactory, {
    // used in error messages
    methodName: 'connect',

     // used to compute Connect's displayName from the wrapped component's displayName.
    getDisplayName: name => `Connect(${name})`,

    // mapStateToProps为空时,不能自动处理View刷新,此时需要用户自己setState来更新view
    shouldHandleStateChanges: Boolean(mapStateToProps),

    // 参数传递到selectorFactory
    initMapStateToProps,
    initMapDispatchToProps,
  })
}

connect方法没做什么事情,调用的connectHOC方法,connectHOC默认实现为connectAdvanced。connectAdvanced最终返回经过包装的React组件,也就是Redux所说的容器型组件。它对React组件生命周期方法进行了一些处理,源码如下。

class Connect extends Component {
      constructor(props, context) {
        super(props, context)

        this.version = version
        this.state = {}
        this.renderCount = 0
        this.store = props[storeKey] || context[storeKey]
        this.propsMode = Boolean(props[storeKey])
        this.setWrappedInstance = this.setWrappedInstance.bind(this)
        this.initSelector()
        this.initSubscription()
      }

      componentDidMount() {
        // shouldHandleStateChanges: Boolean(mapStateToProps)
        // 当mapStateToProps为空时,不能自动处理View刷新,此时需要用户自己setState来更新view
        if (!shouldHandleStateChanges) return

        // 关键在selector.run()方法中,它会将mapStateToProps mapDispatchToProps等扩展的数据添加到React Component的props上。具体看后面makeSelectorStateful的分析
        this.subscription.trySubscribe()
        this.selector.run(this.props)
        // 如果props有改变,shouldComponentUpdate会为true,此时forupdate强制刷新view
        if (this.selector.shouldComponentUpdate) this.forceUpdate()
      }

      componentWillReceiveProps(nextProps) {
        // 具体代码看后面的makeSelectorStateful,主要是判断props有没有更新,如果更新则刷新view
        this.selector.run(nextProps)
      }

      // 判断是否取消掉view的刷新,React生命周期方法
      // selector.shouldComponentUpdate在makeSelectorStateful进行赋值的
      // 如果前后两次props有改变,则设置为true,此时不会拦截view的刷新
      shouldComponentUpdate() {
        return this.selector.shouldComponentUpdate
      }

      // unmount卸载前的调用
      componentWillUnmount() {
        if (this.subscription) this.subscription.tryUnsubscribe()
        this.subscription = null
        this.notifyNestedSubs = noop
        this.store = null
        this.selector.run = noop
        this.selector.shouldComponentUpdate = false
      }

      // wrappedInstance为未经过包装的React组件,也就是Redux所说的UI组件
      getWrappedInstance() {
        return this.wrappedInstance
      }

      setWrappedInstance(ref) {
        this.wrappedInstance = ref
      }

      // 比较关键,具体分析可以看后面对makeSelectorStateful的单独分析
      initSelector() {
        // selectorFactory很复杂,大概就是将stateProps,dispatchProps和原先就有的props合并。
        const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
        // run方法在props有变化时,将shouldComponentUpdate设置为true,React生命周期时可以刷新view。另外将mapStateToProps mapDispatchToProps等扩展的数据添加到React Component的props上。
        this.selector = makeSelectorStateful(sourceSelector, this.store)
        this.selector.run(this.props)
      }

      onStateChange() {
        this.selector.run(this.props)

        if (!this.selector.shouldComponentUpdate) {
          this.notifyNestedSubs()
        } else {
          this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
          this.setState(dummyState)
        }
      }

      addExtraProps(props) {
        // 省略不关键的代码,基本就是将props返回。
        const withExtras = { ...props }
        return withExtras
      }

      // Redux封装了React Component,重写了render方法,对props进行了增强
      render() {
        const selector = this.selector
        selector.shouldComponentUpdate = false

        if (selector.error) {
          throw selector.error
        } else {
          // WrappedComponent是connect()(reactComponent)中传入的React Component
          // Redux对React Component进行了封装,render方法最后调用React的createElement 将props传入
          return createElement(WrappedComponent, this.addExtraProps(selector.props))
        }javascript:void(null)
      }
    }
}

initSelector()方法调用makeSelectorStateful获取selector,并调用它的run方法

function makeSelectorStateful(sourceSelector, store) {
  const selector = {
    run: function runComponentSelector(props) {
      try {
        // 得到props, selector为我们包装的React Component对象
        const nextProps = sourceSelector(store.getState(), props)
        if (nextProps !== selector.props || selector.error) {
          // props有变化,则shouldComponentUpdate为true,会刷新view
          selector.shouldComponentUpdate = true
          // 将mapStateToProps mapDispatchToProps等扩展的数据添加到React Component的props上
          selector.props = nextProps
          selector.error = null
        }
      } catch (error) {
        selector.shouldComponentUpdate = true
        selector.error = error
      }
    }
  }

  return selector
}

connect方法源码十分复杂,我们省略了很多不关键的地方。从源码中我们可以看出

1.mapStateToProps,mapDispatchToProps,最终会经过selectorFactory,与React组件本身的props进行合并。
2.mapStateToProps注册了subscribe,这样每次dispatch时,回调完reducer后,就会调用mapStateToProps,将新的state传递过来。此时再将state合并到组件的props上,会引起props的改变,从而触发一次forceUpdate,从而自动刷新view
3.connect方法对React组件进行了包装,返回一个容器型组件。

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

推荐阅读更多精彩内容