在我们使用Redux这一套解决方案时,其中少不了React-Redux。
当感觉我们编写的组件被我们掌控,操作自如时,其实这一切都是幻觉,React-Redux玩了一个狸猫换太子 ,把我们的组件调包了,怎么会?不急我们慢慢道来!
这篇文章会对React-Redux原理进行分析,以及一些奇技淫巧,最后会解答为什么我们的组件被掉包了!
介绍
React-Redux是一个帮助React和Redux有机关联起来的组件。
在Runing GO Redux(下)中有提到,Redux不能非常方便的用在React当中,不是不能用,只是用起来很蹩脚!我们需要一个组件帮我们把这件事情干的漂亮一些,这个组件就是React-Redux。
React-Redux有两个方法:
Provider
Connet
我们先有一个基本认识,React-Redux有两个React组件,分别在Provider和Connet中实现。
Provider
Provider从调用方法来看,就是一个React组件,接收一个参数:store。
实例
import { Provider } from 'react-redux';
import Store from './store/createStore';
<Provider store={Store}>
...
</Provider>
这里的store就是Redux中的store,很好理解,但Provider只接收store中的dispatch、getState、subscribe这三个方法。
Provider是一个非常普通的React组件,但有一个比较特殊的地方,他使用了两个平时很少用的两个方法,分别是getChildContext(与componentWillMount同级别)和Children(React顶级API之一,与Component同级别)
为了深入探究Provider原理,我们需要先了解一些React知识,不用担心,非常简单!
了解getChildContext
背景
随着我们的应用变的越来越复杂,组件嵌套也变的越来越深,有时最外层数据需要被最里层组件使用,层层嵌套,非常恶心!
叫道理,通过prop向下传递是没问题的,不过很麻烦,如果可以在最外层和最里层之间开一个同(虫洞),那该多好。
React团队也意识到了这个问题,在0.1.4版本中发布了这个属性。
先了解一下getChildContext的基本用法。
实例
// Component A
class A extends Component {
getChildContext() {
return {
info: 'this is FEX·饭记'
}
}
render() {
<B></B>
}
}
A.childContextTypes = {
info: React.PropTypes.string.isRequired
}
// Component B
class B extends Component {
render() {
<div>{this.context.info}</div>
}
}
B.contextTypes = {
user: React.PropTypes.string.isRequired
}
render(<A />, document.body) // <div>this is FEX·饭记</div>
看到上面实例,应该有了大致的了解,但需要注意几点,父组件(A)在内部定义需要的参数:
getChildContext() {
return {
info: 'this is puxiao'
}
}
然后定义参数类型,,如果不定义将无法享受虫洞的待遇:
A.childContextTypes = {
info: React.PropTypes.string.isRequired
}
子组件通过contextTypes定义接收的类型,只有匹配正确的类型,才能访问对应的数据:
// this is FEX·饭记
<div>{this.context.info}</div>
B.contextTypes = {
user: React.PropTypes.string.isRequired
}
通过上面分析可以总结出,使用getChildContext有两个严格的规定。
其一、上层组需要在内部定义数据,并指定数据类型;
其二、下层组件指定数据类型;
当上下层组件对同一条数据类型描述一致时,下层组件才可以引用数据!
了解Children
Children会返回用JSON描述出来的子节点。
<App>
<div>1</div>
<div>2</div>
</App>
其中
<div>1</div>
<div>2</div>
就是App的子节点
需要注意:Children和this.props.children有些区别!
使用this.props.children时,会有以下不同:
没有子节点时返回undefined
有一个子节点时返回Object
有多个子节点时返回Array
Children类似Array,他提供了四个方法:
Children.map// Children方法:
Children.map(this.props.children, function (child) {
return <li>{child}</li>;
})
Children.forEach
类似于Children.map(),但是不返回对象。
Children.count返回children当中的组件总数,和传递给map或者forEach的回调函数的调用次数一致。
Children.only返回children中仅有的子级。否则抛出异常。
仅有的子级表示只能是一个对象,不能是多个对象(数组)。
Children.only(this.props.children[0])
Provider就使用了Children.only()这个方法
Provider源码
render() {
const { children } = this.props
return Children.only(children)
}
根据上面的信息和源码,再看下我们平时的用法,可以知道为什么Provider只能放一个节点,多了报异常!
<Provider store={Store}>
<App />
</Provider>
需要关注一个点,既然Provider使用了Children.only(),最终返回App组件,有没有疑惑,为什么多此一举?带着这个疑问往下看!
Provider和getChildContext、Children是如何产生化学反应?
这个需要从头讲起,Provider作为一个react组件,接收redux返回的store,依靠getChildContext的虫洞效果,使整个组件层都可以拿到store中的方法,为了统一入口,Provider只能接收一个子节点,通过Children.only来做限制!
Connect
Connect接收四个参数:mapStateToProps, mapDispatchToProps, mergeProps, options,返回一个需要接收react组件的方法。
Connect有以下四个步骤:
第一、分配getState中的状态。第二、通过bindActionCreators把action和dispatch绑定在一起。第三、将上面两步合并到一个props中,注入给组件。第四、将我们的组件作为子组件,封装一层,最后返回一个新组件。调用方法:
import { connect } from 'react-redux';
class App extends Component {...}
function mapStateToProps(state) {
return {
visibleTodos: selectTodos(state.visibilityFilter, state.todos),
};
}
export default connect(mapStateToProps)(App);
简单说下这四个参数有什么用,以及一些鲜为人知的用法!
mapStateToProps和mapDispatchToProps
参数类型{ Object|Function }
在很多分析文章中没有提到可以接收Function,但从源码上看确实支持Function,说明可以支持函数式编程。
源码:
configureFinalMapState(store, props) {
const mappedState = mapState(store.getState(), props)
// mappedState就是mapStateToProps
const isFactory = typeof mappedState === 'function'
}
如果mapStateToProps是function,抛开一层(有且仅支持一层嵌套)。
这样来看,可以在mapStateToProps里面做很多事了。。。
实例:
function mapStateToProps() {
return function(state) {
return {
visibleTodos: selectTodos(state.visibilityFilter, state.todos)
}
};
}
mapDispatchToProps同样的套路!
最终mapStateToProps, mapDispatchToProps这两个参数内的数据会被合并成一个props,怎么合并呢?就得靠mergeProps方法了!
mergeProps
参数类型 { Function } 将mapStateToProps,mapDispatchToProps,自身props(this.props)合并在一起。
一般情况下我们不需要重写mergeProps方法,因为Connet提供了默认方法。
源码:
const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({
...parentProps,
...stateProps,
...dispatchProps
})
如果需要修改赋值顺序或做一些预设,还需要自己重写一个!
上面有提到Connect也实现了一个React组件,当传入App时组件被创建。
实例: export default connect(select)(App);
options
{ Object } 用于优化,影响部分数据
pure:{ Boolean = true }
true:表示对两次props做浅比较,不一样才更新;
false:表示每次都更新;
withRef:{ Boolean = false }
true和false的区别在于在最后封装组件时多一个实例的引用
// ref = 'wrappedInstance'
到这里就应该可以返回,用我们传进来的组件App(Dom结构),加上上面处理过的Props,创建了一个新组建(targetComponent),但这还没完,还需要比较targetComponent和我们传进来的组件(App)之间的差异,因为被创建targetComponent时只有Dom结构和Props,我们在生命周期里面的逻辑和绑定函数还没有进来,最后targetComponent继承部分App,除了下面的属性不要,其他统统都要:
{
contextTypes
defaultProps
displayName
getDefaultProps
mixins
propTypes
type
}
{
name
length
prototype
caller
arguments
arity
}
就这样我们的组件App被替换了Connect生成的组件,是不是很出乎你的意外,原来我们的组件已经被掉包了!
传送门:知乎专栏(Redux全系列)