首页 热点资讯 义务教育 高等教育 出国留学 考研考公
您的当前位置:首页正文

react-redux源码解读

2024-12-18 来源:化拓教育网

"react-redux": "^5.0.6"

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
}

也就是说一般我们不需要额外声明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
}

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>;

视图是怎么更新的

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.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 
    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)
显示全文