"react-redux": "^5.0.6"
redux和react之间的桥梁是react的context,react-redux就是基于这点建立二者的联系,react的官方文档Context说的很清楚,实例和说明也很清晰:
通过在MessageList(context提供者)中添加childContextTypes和getChildContext,React会向下自动传递参数,任何组件只要在它的子组件中(这个例子中是Button),就能通过定义contextTypes来获取参数。
如果contextTypes没有定义,那么context将会是个空对象。
但要明确两点:最顶层的context提供者是没有context属性的
context的接受者才有context属性
在react-redux中Provider就是context的提供者,被connect后的容器组件就是context的接受者
按照官网的说法任何组件只要在它的子组件中,就能通过定义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)的由来:
在看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 从哪来 做什么用的
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))
}
}
从目前的形式看,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
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
};
};
然后发现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
经过上面对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
}
从上图可以看到nextStateProps和stateProps是不相等的,为什么呢?
const mapStateToProps = state => {
return {
appList: state.app.appList
};
};
mapStateToProps 总是返回新的对象,所以不能比较mapStateToProps 返回的对象,而是应该比较该对象里的值,也就是areStatePropsEqual做的事情。
该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是相等的
最后在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属性
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是从哪来的?
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
三个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
我们可以根据location的id直接取state里面的数据
mapStateToPropsFactory可以取到初始化的state
官网的解释:
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)
ownProps 在实践中最常见的就是router的配置
export default connect((state, ownProps) => {
console.info("ownProps:", ownProps);
return {
df: state.df
};
})(RuleSet);
或者使用高阶组件把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