在React中引入Redux的一大好处就是将组件的状态移出来,这样React组件就变成了纯View层的对象,可以专心只做View相关的事情了,是所谓“低耦合”。那么,既然React组件不存储状态了, 是否可以将原先的组件都替换为无状态的函数组件呢?

Use stateless component with react-redux

尝试一下,便知行不行:

App.js:

import React from 'react';

const App = (props) => {
    return (
        <div className="App">
            <input type="text" onChange={props.handleInputChange} value={props.input} />
        </div>
    );
};

export default App;

container.js:

import {connect} from 'react-redux'
import App from './App'

const mapStateToProps = (state, ownProps) => {
    return state;
};

const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        handleInputChange: (event) => {
            dispatch({type: 'INPUT_CHANGE', value: event.target.value});
        }
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(App);

reducer.js:

const _defaultState = {
    input: 'javascript',
};

export default function reducer(state = _defaultState, action) {
    switch (action.type) {
        case 'INPUT_CHANGE':
            return {
                input: action.value,
            };

        default:
            return state;
    }
}

以上三段代码分别是无状态的组件、将组件赋予状态的装饰部分以及状态的处理部分(全部代码)。结果是可以和从React.Component继承的类组件一样工作,即connect函数可以接受任何类型的React组件!

How?

这个问题的关键在于connect函数。connect函数首先返回的其实是一个高阶组件,所谓高阶组件其实是类似高阶函数的一个概念,即一个函数,它支持一个组件作为输入,并返回一个组件。当然,把它理解为装饰器也可以,并且在最新的ES7语法中是可以这样写的:

@connect(mapStateToProps, mapDispatchToProps)
class MyComponent {
}

connect返回的高阶组件再以我们的组件函数作为输入,最终返回了一个包装过的组件。

具体它是怎么做的,我们需要看一下connect函数的实现,通过debug,我们可以看到最终它会返回下面这个函数对象(省略了部分原型方法),其中函数输入的Component即React提供的Component类(函数),而WrappedComponent(函数中是通过闭包得到的外部的变量)才是放进去包装的我们自己定义的React组件:

var Connect = function (_Component) {
  _inherits(Connect, _Component);

  function Connect(props, context) {
    _classCallCheck(this, Connect);

    var _this = _possibleConstructorReturn(this, _Component.call(this, 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();
    return _this;
  }

  // ...

  Connect.prototype.render = function render() {
    var selector = this.selector;
    selector.shouldComponentUpdate = false;

    if (selector.error) {
      throw selector.error;
    } else {
      return createElement(WrappedComponent, this.addExtraProps(selector.props));
    }
  };

  return Connect;
}(Component);

可以看到返回的Connect函数其实也是一个React组件,它首先继承了React的Component类(通过_inherits函数,参考关于JavaScript的原型链和继承来看它干了什么),然后它提供了一个自己的render函数,该函数会使用React的createElement函数创建一个新的组件并返回,当然,是在我们定义的WrappedComponent的基础之上(createElement在这里可以看做就是一个往输入组件的实例注入props的函数)。所以,connect函数其实是用这里的Connect组件作为父组件,将输入的组件作为子组件进行了包装。

如果稍微了解下React的源码(可以参考React组件是如何渲染的),就会知道我们写的组件正是会被编译成ReactElement类型的对象的,并且根据输入的组件类型在渲染时会选择调用它的render函数或它自身(输入为函数时,即stateless functional component)。因此,React支持什么类型的组件,connect函数就支持什么类型的组件作为输入。而且,这里createElement的第二个参数就是在输入的组件基础上添加的属性。

以上解释了connect函数可以接受stateless component的原因,但当Redux中状态改变时,作为Connect的子组件,我们输入的组件是怎样被重新渲染的呢?为此,首先要知道作为子组件,在父组件重新渲染时会发生什么。通过对React代码调试可知:

  • 在组件中调用的this.props中的this到底是什么。尝试一下,你会发现其实this.__proto__ === App.prototype,所以this就是App(我们定义的组件)的实例化对象,而this.props就是绑定在实例化对象上的名为props的属性。
  • 实际运行时,所有组件会在第一次加载时都会被先实例化,且每个实例化的对象都有一个指针指向它的子组件的实例化对象。在之后组件的实例需要重新渲染的时候,该组件的render函数返回的组件类并不会被再次实例化,React会将返回的组件类中的属性拿出来并注入到该实例的子组件实例上去。
  • 一个组件无论外面包了多少层父组件,该组件内的this总是指向该组件的实例化对象。

综上,每当Redux中的状态发生变化时,首先触发的是Connect组件的实例中的状态变化,进而会重新渲染Connect组件,即重新调用createElement函数在输入组件的基础上生成新的组件类型(注意此时返回的仍是组件类,而非实例),然后React会把这个新生成的组件类的中的props注入到之前实例化好的子组件的实例对象中,这个子组件就是我们输入的被包装的组件。

结论

react-reduxconnect函数可以接受任何类型的React组件作为输入,输出一个添加(绑定)了包含状态的属性的新的组件。使用无状态的组件可以更好地贴合Redux的意图,降低耦合,代码更简洁明了(相比性能上可能带来的损耗要更重要,参考这个讨论)。