react-redux源码解读

"react-redux": "^5.0.6"

5.0.6 依赖关系

redux和react之间的桥梁是react的context,react-redux就是基于这点建立二者的联系,react的官方文档Context说的很清楚,实例和说明也很清晰:
通过在MessageList(context提供者)中添加childContextTypes和getChildContext,React会向下自动传递参数,任何组件只要在它的子组件中(这个例子中是Button),就能通过定义contextTypes来获取参数。
如果contextTypes没有定义,那么context将会是个空对象。

但要明确两点:最顶层的context提供者是没有context属性的

MessageList

context的接受者才有context属性

Button

在react-redux中Provider就是context的提供者,被connect后的容器组件就是context的接受者

Provider
connect

按照官网的说法任何组件只要在它的子组件中,就能通过定义contextTypes来获取参数。那么所有的组只要定义了contextTypes,就会拿到context,

connectAdvanced.js

return function wrapWithConnect(WrappedComponent) {
  class Connect extends Component {
          getChildContext() {
        // If this component received store from props, its subscription should be transparent
        // to any descendants receiving store+subscription from context; it passes along
        // subscription passed to it. Otherwise, it shadows the parent subscription, which allows
        // Connect to control ordering of notifications to flow top-down.
        const subscription = this.propsMode ? null : this.subscription
        return { [subscriptionKey]: subscription || this.context[subscriptionKey] }
      }
  }
Connect.WrappedComponent = WrappedComponent
Connect.displayName = displayName
Connect.childContextTypes = childContextTypes
Connect.contextTypes = contextTypes
Connect.propTypes = contextTypes
return hoistStatics(Connect, WrappedComponent)
}

Connect定义了contextTypes所以有context属性,但同时也有childContextTypes和getChildContext方法,难道它也是context的提供者,带着疑问上路

1. 经过react-redux包装后的组件名Connect(AddTodo)的由来:
Connect(AddTodo)

在看selector是怎么来的时候恰好发现了displayName的出现

export default function connectAdvanced(
selectorFactory,
  // options object:
  {
    getDisplayName = name => `ConnectAdvanced(${name})`,

    methodName = 'connectAdvanced',

    renderCountProp = undefined,

    shouldHandleStateChanges = true,

    storeKey = 'store',

    withRef = false,

    ...connectOptions
  } = {}
) {
return function wrapWithConnect(WrappedComponent) {
    ...
    const wrappedComponentName = WrappedComponent.displayName
      || WrappedComponent.name
      || 'Component'//一般我们自己的展示组件不会有displayName,除非显示声明displayName,
      //但是每个组件都有name属性,无论是class组件还是函数式组件,
      //class本质上还是构造函数,函数都有name属性,那就是function的name

    //用vscode F12转到定义是到上面的connectAdvanced函数参数的getDisplayName,其实该函数是在
    //connect.js connectHOC(selectorFactory,{getDisplayName: name => `Connect(${name})`,})传递进去的
    //实参,返回的就是"Connect(AddTodo)"
    const displayName = getDisplayName(wrappedComponentName)

    const selectorFactoryOptions = {
      ...connectOptions,
      getDisplayName,
      methodName,
      renderCountProp,
      shouldHandleStateChanges,
      storeKey,
      withRef,
      displayName,
      wrappedComponentName,
      WrappedComponent
    }
   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)

        invariant(this.store,
          `Could not find "${storeKey}" in either the context or props of ` +
          `"${displayName}". Either wrap the root component in a <Provider>, ` +
          `or explicitly pass "${storeKey}" as a prop to "${displayName}".`
        )

        this.initSelector()
        this.initSubscription()
      }
      ...
      initSelector() {
        const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
        this.selector = makeSelectorStateful(sourceSelector, this.store)
        this.selector.run(this.props)
      }
   }
}

    Connect.WrappedComponent = WrappedComponent
    Connect.displayName = displayName //就是上面代码拼接的容器组件名称Connect(AddTodo)
    Connect.childContextTypes = childContextTypes
    Connect.contextTypes = contextTypes
    Connect.propTypes = contextTypes
}

查阅了react的官方,发现我上面的理解是正确的displayName

The displayName string is used in debugging messages. Usually, you don’t need to set it explicitly because it’s inferred from the name of the function or class that defines the component. You might want to set it explicitly if you want to display a different name for debugging purposes or when you create a higher-order component, see Wrap the Display Name for Easy Debugging for details.

也就是说一般我们不需要额外声明displayname去指定组件的名称,如果声明了组件的displayName,那么最后展示出来的组件名称就是displayName,而不是name

从react的扣扣声明也可以看出来,displayName是静态属性,不是实例属性

    interface ComponentClass<P = {}> {
        new (props?: P, context?: any): Component<P, ComponentState>;
        propTypes?: ValidationMap<P>;
        contextTypes?: ValidationMap<any>;
        childContextTypes?: ValidationMap<any>;
        defaultProps?: Partial<P>;
        displayName?: string;//看这里
    }

从react-redux的connectAdvanced.js Connect组件的声明也可以看出来,这里的displayName就覆盖了原来function或class的name,使得最后我们看到经过react-redux connect后的容器组件名为Connect(AddTodo)的样子

    Connect.WrappedComponent = WrappedComponent
    Connect.displayName = displayName
    Connect.childContextTypes = childContextTypes
    Connect.contextTypes = contextTypes
    Connect.propTypes = contextTypes
2. selector 从哪来 做什么用的
todos TodoList connectAdvanced.js 的Connect render函数
      addExtraProps(props) {
        if (!withRef && !renderCountProp && !(this.propsMode && this.subscription)) return props
        // make a shallow copy so that fields added don't leak to the original selector.
        // this is especially important for 'ref' since that's a reference back to the component
        // instance. a singleton memoized selector would then be holding a reference to the
        // instance, preventing the instance from being garbage collected, and that would be bad
        const withExtras = { ...props }
        if (withRef) withExtras.ref = this.setWrappedInstance
        if (renderCountProp) withExtras[renderCountProp] = this.renderCount++
        if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription
        return withExtras
      }

      render() {
        const selector = this.selector
        selector.shouldComponentUpdate = false

        if (selector.error) {
          throw selector.error
        } else {
          return createElement(WrappedComponent, this.addExtraProps(selector.props))
        }
      }
TodoList 没有添加额外的属性

从目前的形式看,selector的作用就是根据一些条件计算props到展示组件,initSelector被调用有两个地方,一个是容器组件Connect被初始化的时候(就是Connect类的构造函数中),还有一处是在componentWillUpdate的时候

1.初始化
根据mapStateToProps mapStateToProps 以及reducers中自己定义的默认state去初始化state和dispatch

const mapStateToProps = (state) => ({
  todos: getVisibleTodos(state.todos, state.visibilityFilter)
})

const mapDispatchToProps = {
  onTodoClick: toggleTodo
}

2.componentWillUpdate再次调用initSelector函数

createElement
接口:

function createElement<P, T extends Component<P, ComponentState>, C extends ComponentClass<P>>(
        type: ClassType<P, T, C>,
        props?: ClassAttributes<T> & P,
        ...children: ReactNode[]): CElement<P, T>;

官网:
根据给定的类型创建并返回新的 React element 。参数type既可以是一个html标签名称字符串(例如'div' 或 'span'),也可以是一个 React component 类型(一个类或一个函数)。用 JSX 编写的代码会被转换成用 React.createElement()
实现。如果使用了JSX,你通常不会直接调用 React.createElement()。

视图是怎么更新的

mapStateToProps

箭头所指的地方就是我们在组件自定义的函数mapStateToProps

const mapStateToProps = state => {
  console.info('mapStateToProps');
  return {
    appList: state.app.appList
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(AppList);

redux createStore.js dispatch

  function dispatch(action) {
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

本来以为是在这里对前后的state做比较,从而决定监听函数要不要被触发,其实只要触发dispatch,subscribe监听的函数就会被触发,那么到底是怎么触发视图更新的?

class Connect extends Component {
  constructor(props, context) {
    this.initSubscription();//初始化监听器
  }

  initSubscription() {
    if (!shouldHandleStateChanges) return;

    this.subscription = new Subscription(
      this.store,
      parentSub,
      this.onStateChange.bind(this)
    );

    this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(
      this.subscription
    );
  }

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

    if (!this.selector.shouldComponentUpdate) {
      this.notifyNestedSubs();
    } else {
      this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate;
      this.setState(dummyState);//通过setState触发Connect的更新
    }
  }

      componentDidMount() {
        if (!shouldHandleStateChanges) return
        this.subscription.trySubscribe()//在这里触发对redux的监听
        this.selector.run(this.props)
        if (this.selector.shouldComponentUpdate) this.forceUpdate()
      }
}

export default class Subscription {
  constructor(store, parentSub, onStateChange) {
    this.store = store
    this.parentSub = parentSub
    this.onStateChange = onStateChange
    this.unsubscribe = null
    this.listeners = nullListeners
  }

  addNestedSub(listener) {
    this.trySubscribe()
    return this.listeners.subscribe(listener)
  }

  notifyNestedSubs() {
    this.listeners.notify()
  }

  isSubscribed() {
    return Boolean(this.unsubscribe)
  }

  trySubscribe() {
    if (!this.unsubscribe) {
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.onStateChange)
        : this.store.subscribe(this.onStateChange)//使用redux监听onStateChange,从而在dispatch的时候onStateChange都会被触发
 
      this.listeners = createListenerCollection()
    }
  }

  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe()
      this.unsubscribe = null
      this.listeners.clear()
      this.listeners = nullListeners
    }
  }
}

从上面代码可以看出在connect(mapStateToProps, mapDispatchToProps)(AppList);的时候constructor会去初始化initSubscription,在componentDidMount触发对redux的监听。通过setState触发Connect的更新。

前后的state对比是在什么地方放?
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) {//对比前后state是否相等
          selector.shouldComponentUpdate = true
          selector.props = nextProps
          selector.error = null
        }
      } catch (error) {
        selector.shouldComponentUpdate = true
        selector.error = error
      }
    }
  }

  return selector
}

上述代码决定selector.shouldComponentUpdate的值,为true的话,onStateChange就会setState,从而触发视图的更新

问题:前后的state的对比,应该只是对比我们的关心的数据,也就是mapStateToProps 返回的数据,这个有待研究

const mapStateToProps = state => {
  return {
    appList: state.app.appList
  };
};
前后对比state

然后发现nextState===state居然不相等,再去看下combineReducers

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

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

    let hasChanged = false
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey//前后state进行对比
    }
    return hasChanged ? nextState : state//有一个子的属性改变了,就会返回新的全局state
  }

因为我每次返回的是新的引用,所以返回新的全局state

export default (state = initialState, action) => {
  const { type, payload } = action;
  switch (type) {
    case ALTER_ITEM:
      const { appList } = state;
      //   appList[1].name = payload;直接修改 但是appitem 不会刷新

      //   const item = { ...appList[1] };
      //   item.name = payload;
      //   appList[1] = item;

      //修改了一个 对应修改的那个appitem会刷新,但是这么操作很麻烦

      return { ...state, appList };
    //这么返回就相当于{...state},app的引用变了,但是属性appList的引用还是没改,AppList extends React.Component,不会走render,但是每次都会执行mapStateToProps
    default:
      return state;
  }
};
handleSubsequentCalls对比全局state
handleSubsequentCalls对比全局state

经过上面对combineReducers的分析,全局state的一个属性的引用发生了改变,也就是{app:{appList:[]}}}发生了改变,那么var stateChanged = !areStatesEqual(nextState, state);就是true,进入到handleNewState函数

  function handleNewState() {
//根据全局state和自己在容器组件里定义的mapStateToProps得到本容器组件需要的数据
    const nextStateProps = mapStateToProps(state, ownProps)
// 对比前后容器组件关心的数据是否相等,这是一次浅比较
    const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps)
    stateProps = nextStateProps

    if (statePropsChanged)
      mergedProps = mergeProps(stateProps, dispatchProps, ownProps)

    return mergedProps
  }
对mapStateToProps返回的数据进行比较

从上图可以看到nextStateProps和stateProps是不相等的,为什么呢?

const mapStateToProps = state => {
  return {
    appList: state.app.appList
  };
};

mapStateToProps 总是返回新的对象,所以不能比较mapStateToProps 返回的对象,而是应该比较该对象里的值,也就是areStatePropsEqual做的事情。


areStatePropsEqual对mapStateToProps 返回的对象的值进行比较

该shallowEqual跟react库的实现几乎一模一样,首先判断两个对象的引用是否相同,在判断两个对象key的个数是否相等,再判断两个对象里面的key是否都一样,以及相同的key对应的值是否相同,这里的相同是用===判断的,也就是引用类型的值不改变引用的话,这里的判断是相等的,也就不会触发this.setState去更新Connect组件,也就不会容器组件的更新,而我的测试代码是

 case ALTER_ITEM:
      const { appList } = state;
      return { ...state, appList };

并没有改变appList 的引用,所以nextStateProps.appList===stateProps.appList返回true,使用shallowEqual比较mapStateToProps 返回的appList跟上次一的appList是相等的

返回旧的props

最后在makeSelectorStateful run函数对比前后合并后的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
}

上面代码的nextProps 就是最后合并的props,包括三部分,state props,dispatch props,own props

  • own props就是从父组件传入的props
    比如react-router传入的props,history location match属性


    从Route传递到Connect组件的props
  • state props就是容器组件mapStateToProps返回的props

const mapStateToProps = state => {
  return {
    appList: state.app.appList
  };
};
  • dispatch props就是容器组件mapDispatchToProps返回的props
const mapDispatchToProps = dispatch =>
  bindActionCreators(
    {
      alterAppItem,
      plusOne
    },
    dispatch
  );

最后容器组件得到哪些props?


容器组件所有的props

这些props是从哪来的?
Connect 的render函数

      render() {
        const selector = this.selector
        selector.shouldComponentUpdate = false

        if (selector.error) {
          throw selector.error
        } else {
          return createElement(WrappedComponent, this.addExtraProps(selector.props))
        }
      }

也就是从selector.props来的,

function makeSelectorStateful(sourceSelector, store) {
  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.props来源
          selector.error = null
        }
      } catch (error) {
        selector.shouldComponentUpdate = true
        selector.error = error
      }
    }
  }
  return selector
}

sourceSelector根据父组件传递的props和redux的state,通过容器组件的mapStateToProps和mapDispatchToProps函数的筛选,计算得到最后容器组件的props,而上述代码的sourceSelector就是pureFinalPropsSelector

pureFinalPropsSelector

三个props是在哪merge的

  function handleFirstCall(firstState, firstOwnProps) {
    state = firstState
    ownProps = firstOwnProps
    stateProps = mapStateToProps(state, ownProps)
    dispatchProps = mapDispatchToProps(dispatch, ownProps)
    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    hasRunAtLeastOnce = true
    return mergedProps
  }

export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
  return { ...ownProps, ...stateProps, ...dispatchProps }
}

上述代码可以看出就是将三个props展开,放到新的对象里,也就是检测出有MapStateToProps或者mapDispatchToProps或者父组件传进来的props的改变就会返回新的合并后的props的引用,从而触发Connect的setState函数,从而容器组件得到更新

mapStateToProps有第二个参数ownProps

ownProps

我们可以根据location的id直接取state里面的数据

mapStateToPropsFactory可以取到初始化的state

mapStateToPropsFactory

官网的解释:
Factory functions
Factory functions can be used for performance optimizations

import { addTodo } from './actionCreators'

function mapStateToPropsFactory(initialState, initialProps) {
  const getSomeProperty= createSelector(...);
  const anotherProperty = 200 + initialState[initialProps.another];
  return function(state){
    return {
      anotherProperty,
      someProperty: getSomeProperty(state),
      todos: state.todos
    }
  }
}

function mapDispatchToPropsFactory(initialState, initialProps) {
  function goToSomeLink(){
    initialProps.history.push('some/link');
  }
  return function(dispatch){
    return {
      addTodo
    }
  }
}


export default connect(mapStateToPropsFactory, mapDispatchToPropsFactory)(TodoApp)

React-Redux源码分析
react-redux源码

ownProps 在实践中最常见的就是router的配置

export default connect((state, ownProps) => {
  console.info("ownProps:", ownProps);
  return {
    df: state.df
  };
})(RuleSet);
ownProps

或者使用高阶组件把connect高阶组件包起来,这样在mapStateToProps的第二个参数ownProps也能拿到自定义高阶组件拿到的props,比如

logic.js

import React from "react";

export default function(WrappedComponent) {
  return class Logic extends React.Component {
    static displayName = "HOC(Logic)";
    render() {
      return <WrappedComponent {...this.props} currentSelected={1} />;
    }
  };
}

使用高阶组件logic.js

export default logic(
  connect((state, ownProps) => {
    console.info("ownProps:", ownProps);
    return {
      df: state.df
    };
  })(RuleSet)
);

如果使用了 ownProps(在 mapStateToProps 中声明了),当其发生变化时(浅比较),同样会触发 mapStateToProps 的重新计算,这一般会引发展示型组件的重新渲染

React 实践心得:react-redux 之 connect 方法详解
ownProps in mapStateToProps

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

推荐阅读更多精彩内容