1

react-redux 源码解读

[TOC]

前置知识

阅读本篇文章前,请先确认你是否了解以下知识:

  • react
  • redux
  • 高阶组件
  • react diff 机制

其中高阶组件如果你不太了解也无所谓,你只需要知道,高阶组件就是一个工厂函数,它接收一个组件类(或者函数组件),返回一个被修改后的新的组件类。connect 就是一个高阶组件。

文章内会使用的简写

  • hoc: 高阶组件(higher order component)
  • scu: shouldComponentUpdate

Issues

我们知道,react-redux 为开发者提供了 redux 到 react 的 binding。本文并不逐行地对源码进行细致分析,不如说是基于以下几个问题,对源码进行大致的扫览。

我们把关注点放在:

  • connect 是通过什么方式来连接 store 的?
  • 怎么分发 state tree 到子组件的?
    provider 仅仅只是保持了一个 store 实例,即使 store 中的 state tree 变化了,由于 react diff 阶段只做浅比较,仅比较对象引用,故 provider.props.store 被视为未发生变化,那就无法把新的 state tree 分发到子组件了。

我们知道 connect 还接收若干配置函数,用来 mapXxToProps ,以及设置是否进行 pure 优化等。这些没什么意思,我们就不 care 了。

两个核心 api

react-redux 只有两个核心的部分

  • Provider
  • connect

Provider

Provider 的作用仅仅是维护一个 store 实例,并使用 context , 为分发 state tree 提供了机制

connect

connect 顾名思义,连接 container 和 store ,使得 container 能响应 state tree 的变化。

connect 本身是一个高阶组件,它调用了 connectAdvanced 这么一个工厂函数。

而上面提到的两个问题,全都由 connectAdvanced 封装了。

connectAdvanced

connectAdvanced 是一个高阶组件的工厂函数,它返回一个 hoc (高阶组件),这个 hoc 最终返回一个 Connect(WrappedComponent) 的组件。

connect 是通过什么方式来连接 store 的?

在这个 Connect(WrappedComponent) 中,声明了 getChildContext 函数,通过它来获取到 Provider 中共享的 store 实例。

NOTE : 理论上,Provider 下任何一个子组件,只要我们也去声明了 getChildContext ,那么它就和 connect 了一样,可以得到 store 实例了。

connect 怎么分发 state tree 到子组件的?

一句话,主要通过 store.subscribe 接口和组件 props 变更自动 re-render 机制。

在 react-redux 中,我们定义了这么几个东西,帮助我们来做到上面几件事情。

  • selector
  • subscriptions

每个 Connect(WrappedComponent) 在构造函数中都需要 init 上述两个实体。

selector

selector 主要由 selectorFactory 构造,selector 内部包含

  • 一个计算新 props 的生成函数
    sourceSelector(store.getState(), props)
    其中 props 是 selector 之前缓存的,后续会直接拿来和 store.getState() 比较引用
  • 一个 shouldComponentUpdate 的标识
    Connect(WrappedComponent) 内部是直接使用它来判断 scu 的
shouldComponentUpdate() {
  return this.selector.shouldComponentUpdate
}    

源码如下

const selector = {
    // 实际 run 的逻辑又是在 SelectorFactory 里定义的,这里只是做 scu
    run: function runComponentSelector(props) {
      try {
        const nextProps = sourceSelector(store.getState(), props)
        // connect 的 scu 实际逻辑在这里定义了
        // 比较 run 之后两次 props 的引用是否仍然保持相等
        if (nextProps !== selector.props || selector.error) {
          selector.shouldComponentUpdate = true
          selector.props = nextProps
          selector.error = null
        }
      } catch (error) {
        selector.shouldComponentUpdate = true
        selector.error = error
      }
    }
  }

调用 selector.run 的时候,主要做了两件事情

  • 根据之前缓存的 prevProps 和当前 redux store 实例中的 statet tree 生成新的 props。
  • 通过比较引用,更新 shouldComponentUpdate 判断标识

Connect(WrappedComponent) 会在三处调用 selector.run

  • componentDidMount
  • componentWillReceiveProps
  • onStateChanged

之后 React 就会自动向子组件分发变更后的 props ,实现 re-render。

subcription

我们在上面提到过 Provider 本身是无法通知 state tree 的变化的,于是为了监听 state tree 变化,我们需要通过 store.subcribe 接口,来向 Provider 的 store 实例注册一些监听器。

我们已经知道,redux 中,store.subcribe 允许用户注册监听器,这些监听器会在每次 dispatch 执行结束后遍历触发。

于是在 react-redux 中, Connect(WrappedComponent) 中的 subcription 最终就是被注册到 store 中。

initSubscription() {
    // ...省略一些无关代码
    this.subscription = new Subscription(this.store, parentSub, this.onStateChange.bind(this))
}

其中,

  • parentSub 是为了在嵌套的 connect 中嵌套执行 subscription。
  • new Subscription 主要构造了一个包含 trySubscribe 方法的对象
trySubscribe() {
    if (!this.unsubscribe) {
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.onStateChange)
        : this.store.subscribe(this.onStateChange)

      this.listeners = createListenerCollection()
    }
  }

Connect(WrappedComponent)componentDidMounttrySubscribe,在 componentWillUnmounttryUnsubscribe

而 trySubscribe 的主要逻辑就是将 onStateChange 注册到 redux.store 中。

而我们已经说过,onStateChange 主要就是执行 selector.run

onStateChange() {
  // 得到新的 props 和 state
  this.selector.run(this.props)

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

于是,每次 state tree 发生变化后,或者更准确地说,每次 dispatch 成功后,redux.store 都会通过注册好的 subscription,执行 Connect(WrappedComponent) 中的 onStateChange ,再由 selector.run 来判断是否需要生成新的 props。


tinkgu
503 声望20 粉丝

{{user.signature}}