"react-redux": "^5.0.6"
MessageListcontext的接受者才有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)