React-Redux你做了什么?

236 阅读6分钟
原文链接: zhuanlan.zhihu.com

在我们使用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全系列)