前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >react-redux源码解读

react-redux源码解读

作者头像
ayqy贾杰
发布2019-06-12 14:15:01
9230
发布2019-06-12 14:15:01
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬黯羽轻扬

写在前面

react-redux作为胶水一样的东西,似乎没有深入了解的必要,但实际上,作为数据层(redux)与UI层(react)的连接处,其实现细节对整体性能有着决定性的影响。组件树胡乱update的成本,要比多跑几遍reducer树的成本高得多,所以有必要了解其实现细节

仔细了解react-redux的好处之一是可以对性能有基本的认识,考虑一个问题:

dispatch({type: 'UPDATE_MY_DATA', payload: myData})

组件树中某个角落的这行代码,带来的性能影响是什么?几个子问题:

  • 1.导致哪些reducer被重新计算了?
  • 2.引发的视图更新从哪个组件开始?
  • 3.哪些组件的render被调用了?
  • 4.每个叶子组件都被diff波及了吗?为什么?

如果无法准确回答这几个问题,对性能肯定是心里没底的

一.作用

首先,明确redux只是一个数据层,而react只是一个UI层,二者之间是没有联系的

如果左右手分别拿着redux和react,那么实际情况应该是这样的:

  • redux把数据结构(state)及各字段的计算方式(reducer)都定好了
  • react根据视图描述(Component)把初始页面渲染出来

可能是这个样子:

       redux      |      reactmyUniversalState  |  myGreatUI
 human           |    noOneIsHere
   soldier       |
     arm         |
   littleGirl    |
     toy         |
 ape             |    noOneIsHere
   hoho          |
 tree            |    someTrees
 mountain        |    someMountains
 snow            |    flyingSnow

左边redux里什么都有,但是react不知道,只显示了默认元素(没有没有数据),有一些组件局部state和零散的props传递,页面就像一帧静态图,组件树看起来只是由一些管道连接起来的大架子

现在我们考虑把react-redux加进来,那么就会变成这样子:

             react-redux
      redux     -+-     reactmyUniversalState  |  myGreatUI
           HumanContainer
 human          -+-   humans
   soldier       |      soldiers
           ArmContainer
     arm        -+-       arm
   littleGirl    |      littleGirl
     toy         |        toy
           ApeContainer
 ape            -+-   apes
   hoho          |      hoho
          SceneContainer
 tree           -+-   Scene
 mountain        |     someTrees
 snow            |     someMountains
                        flyingSnow

注意,Arm交互比较复杂,不适合由上层(HumanContainer)控制,所以出现了嵌套Container

Container把redux手里的state交给react,这样初始数据就有了,那么如果要更新视图呢?

Arm.dispatch({type: 'FIRST_BLOOD', payload: warData})

有人打响了第一枪,导致soldier挂了一个(state change),那么这些部分要发生变化:

                react-redux
         redux     -+-     react
myNewUniversalState  |  myUpdatedGreatUI
             HumanContainer
    human          -+-   humans
      soldier       |      soldiers
                    |      diedSoldier
               ArmContainer
        arm        -+-       arm
                    |          inactiveArm

页面上出现一个挂掉的soldier和一支掉地上的arm(update view),其它部分(ape, scene)一切安好

上面描述的就是react-redux的作用:

  • 把state从redux传递到react
  • 并负责在redux state change后update react view

那么猜也知道,实现分为3部分:

  1. 给管道连接起来的大架子添上一个个小水源(通过Container把state作为props注入下方view)
  2. 让小水源冒水(监听state change,通过Container的setState来更新下方view)
  3. 不小水源不要乱冒(内置性能优化,对比缓存的state, props看有没有必要更新)

二.关键实现

源码关键部分如下:

// from: src/components/connectAdvanced/Connect.onStateChange
onStateChange() {
 // state change时重新计算props
 this.selector.run(this.props) // 当前组件不用更新的话,通知下方container检查更新
 // 要更新的话,setState空对象强制更新,延后通知到didUpdate
 if (!this.selector.shouldComponentUpdate) {
   this.notifyNestedSubs()
 } else {
   this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
   // 通知Container下方的view更新
//!!! 这里是把redux与react连接起来的关键
   this.setState(dummyState)
 }
}

最重要的那个setState就在这里,dispatch action后视图更新的秘密是这样:

1.dispatch action
2.redux计算reducer得到newState
3.redux触发state change(调用之前通过store.subscribe注册的state变化监听器)
4.react-redux顶层Container的onStateChange触发
 1.重新计算props
 2.比较新值和缓存值,看props变了没,要不要更新
 3.要的话通过setState({})强制react更新
 4.通知下方的subscription,触发下方关注state change的Container的onStateChange,检查是否需要更新view

第3步里,react-redux向redux注册store change监听的动作发生在connect()(myComponent)时,事实上react-redux只对顶层Container直接监听了redux的state change,下层Container都是内部传递通知的,如下:

// from: src/utils/Subscription/Subscription.trySubscribe
trySubscribe() {
 if (!this.unsubscribe) {
   // 没有父级观察者的话,直接监听store change
   // 有的话,添到父级下面,由父级传递变化
   this.unsubscribe = this.parentSub
     ? this.parentSub.addNestedSub(this.onStateChange)
     : this.store.subscribe(this.onStateChange)
 }
}

这里不直接监听redux的state change,而非要自己维护Container的state change listener,是为了实现次序可控,例如上面提到的:

// 要更新的话,延后通知到didUpdate
this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate

这样保证了listener触发顺序是按照组件树层级顺序的,先通知大子树更新,大子树更新完毕后,再通知小子树更新

更新的整个过程就是这样,至于“通过Container把state作为props注入下方view”这一步,没什么好说的,如下:

// from: src/components/connectAdvanced/Connect.render
render() {
 return createElement(WrappedComponent, this.addExtraProps(selector.props))
}

根据WrappedComponent需要的state字段,造一份props,通过React.createElement注入进去。ContainerInstance.setState({})时,这个render函数被重新调用,新的props被注入到view,view will receive props…视图更新就真正开始了

三.技巧

让纯函数拥有状态

function makeSelectorStateful(sourceSelector, store) {
 // wrap the selector in an object that tracks its results between runs.
 const selector = {
   run: function runComponentSelector(props) {
     try {
       const nextProps = sourceSelector(store.getState(), props)
       if (nextProps !== selector.props || selector.error) {
         selector.shouldComponentUpdate = true
         selector.props = nextProps
         selector.error = null
       }
     } catch (error) {
       selector.shouldComponentUpdate = true
       selector.error = error
     }
   }
 } return selector
}

把纯函数用对象包起来,就可以有局部状态了,作用和new Class Instance类似。这样就把纯的部分与不纯的部分分离开了,纯的依然纯,不纯的在外面,class不如这个干净

默认参数与对象解构

function connectAdvanced(
 selectorFactory,
 // options object:
 {
   getDisplayName = name => `ConnectAdvanced(${name})`,
   methodName = 'connectAdvanced',
   renderCountProp = undefined,
   shouldHandleStateChanges = true,
   storeKey = 'store',
   withRef = false,
   // additional options are passed through to the selectorFactory
   ...connectOptions
 } = {}
) {
 const selectorFactoryOptions = {
   // 展开 还原回去
   ...connectOptions,
   getDisplayName,
   methodName,
   renderCountProp,
   shouldHandleStateChanges,
   storeKey,
   withRef,
   displayName,
   wrappedComponentName,
   WrappedComponent
 }
}

可以简化成这样:

function f({a = 'a', b = 'b', ...others} = {}) {
   console.log(a, b, others);
   const newOpts = {
     ...others,
     a,
     b,
     s: 's'
   };
   console.log(newOpts);
}
// test
f({a: 1, c: 2, f: 0});
// 输出
// 1 "b" {c: 2, f: 0}
// {c: 2, f: 0, a: 1, b: "b", s: "s"}

这里用到3个es6+小技巧:

  • 默认参数。防止解构时右边undefined报错
  • 对象解构。把剩余属性都包进others对象里
  • 展开运算符。把others展开,属性merge到目标对象上

默认参数是es6特性,没什么好说的。对象解构是Stage 3 proposal,...others是其基本用法。展开运算符把对象展开,merge到目标对象上,也不复杂

比较有意思的是这里把对象解构和展开运算符配合使用,实现了这种需要对参数做打包-还原的场景,如果不用这2个特性,可能需要这样做:

function connectAdvanced(
 selectorFactory,
 connectOpts,
 otherOpts
) {
 const selectorFactoryOptions = extend({},
   otherOpts,
   getDisplayName,
   methodName,
   renderCountProp,
   shouldHandleStateChanges,
   storeKey,
   withRef,
   displayName,
   wrappedComponentName,
   WrappedComponent
 )
}

需要清楚地区分connectOptsotherOpts,实现上会麻烦一些,组合运用这些技巧的话,代码相当简练

另外还有1个es6+小技巧:

addExtraProps(props) {
 //! 技巧 浅拷贝保证最少知识
 //! 浅拷贝props,不把别人不需要的东西传递出去,否则影响GC
 const withExtras = { ...props }
}

多一份引用就多一份内存泄漏的风险,不需要的不应该给(最少知识

参数模式匹配

function match(arg, factories, name) {
 for (let i = factories.length - 1; i >= 0; i--) {
   const result = factories[i](arg)
   if (result) return result
 } return (dispatch, options) => {
   throw new Error(`Invalid value of type ${typeof arg} for ${name} argument when connecting component ${options.wrappedComponentName}.`)
 }
}

其中factories是这样:

// mapDispatchToProps
[
 whenMapDispatchToPropsIsFunction,
 whenMapDispatchToPropsIsMissing,
 whenMapDispatchToPropsIsObject
]
// mapStateToProps
[
 whenMapStateToPropsIsFunction,
 whenMapStateToPropsIsMissing
]

针对参数的各种情况建立一系列case函数,然后让参数依次流经所有case,匹配任意一个就返回其结果,都不匹配就进入错误case

类似于switch-case,用来对参数做模式匹配,这样各种case都被分解出去了,各自职责明确(各case函数的命名非常准确)

懒参数

function wrapMapToPropsFunc() {
 // 猜完立即算一遍props
 let props = proxy(stateOrDispatch, ownProps)
 // mapToProps支持返回function,再猜一次
 if (typeof props === 'function') {
   proxy.mapToProps = props
   proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
   props = proxy(stateOrDispatch, ownProps)
 }
}

其中,懒参数是指:

// 把返回值作为参数,再算一遍props
if (typeof props === 'function') {
 proxy.mapToProps = props
 proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
 props = proxy(stateOrDispatch, ownProps)
}

这样实现和react-redux面临的场景有关,支持返回function主要是为了支持组件实例级(默认是组件级)的细粒度mapToProps控制。这样就能针对不同组件实例,给不同的mapToProps,支持进一步提升性能

从实现上来看,相当于把实际参数延后了,支持传入一个参数工厂作为参数,第一次把外部环境传递给工厂,工厂再根据环境造出实际参数。添了工厂这个环节,就把控制粒度细化了一层(组件级的细化到了组件实例级,外部环境即组件实例信息)

P.S.关于懒参数的相关讨论见https://github.com/reactjs/react-redux/pull/279

四.疑问

1.默认的props.dispatch哪里来的?

connect()(MyComponent)

不给connect传任何参数,MyComponent实例也能拿到一个prop叫dispatch,是在哪里偷偷挂上的?

function whenMapDispatchToPropsIsMissing(mapDispatchToProps) {
 return (!mapDispatchToProps)
   // 就是这里挂上去的,没传mapDispatchToProps的话,默认把dispatch挂到props上
   ? wrapMapToPropsConstant(dispatch => ({ dispatch }))
   : undefined
}

默认内置了一个mapDispatchToProps = dispatch => ({ dispatch }),所以组件props身上有dispatch,如果指定了mapDispatchToProps,就不给挂了

2.多级Container会不会面临性能问题?

考虑这种场景:

App
 HomeContainer
   HomePage
     HomePageHeader
       UserContainer
         UserPanel
           LoginContainer
             LoginButton

出现了嵌套的container,那么在HomeContainer关注的state发生变化时,会不会走很多遍视图更新?比如:

HomeContainer update-didUpdate
UserContainer update-didUpdate
LoginContainer update-didUpdate

如果是这样,轻轻一发dispatch,导致3个子树更新,感觉性能要炸了

实际上不是这样。对于多级Container,走两遍的情况确实存在,只是这里的走两遍不是指视图更新,而是说state change通知

上层Container在didUpdate后会通知下方Container检查更新,可能会在小子树再走一遍。但在大子树更新的过程中,走到下方Container时,小子树在这个时机就开始更新了,大子树didUpdate后的通知只会让下方Container空走一遍检查,不会有实际更新

检查的具体成本是分别对state和props做===比较和浅层引用比较(也是先===比较),发现没变就结束了,所以每个下层Container的性能成本是两个===比较,不要紧。也就是说,不用担心使用嵌套Container带来的性能开销

五.源码分析

Github地址:https://github.com/ayqy/react-redux-5.0.6

P.S.注释依然足够详尽。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-10-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 一.作用
  • 二.关键实现
  • 三.技巧
    • 让纯函数拥有状态
      • 默认参数与对象解构
        • 参数模式匹配
          • 懒参数
          • 四.疑问
            • 1.默认的props.dispatch哪里来的?
              • 2.多级Container会不会面临性能问题?
              • 五.源码分析
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档