React-redux: React.js 和 Redux 架构的结合

通过Redux 架构理解我们了解到 Redux 架构的 store、action、reducers 这些基本概念和工作流程。我们也知道了 Redux 这种架构模式可以和其他的前端库组合使用,而 React-redux 正是把 Redux 这种架构模式和 React.js 结合起来的一个库。

Context

在 React 应用中,数据是通过 props 属性自上而下进行传递的。如果我们应用中的有很多组件需要共用同一个数据状态,可以通过状态提升的思路,将共同状态提升到它们的公共父组件上面。但是我们知道这样做是非常繁琐的,而且代码也是难以维护的。这时会考虑使用 Context,Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。也就是说在一个组件如果设置了 context,那么它的子组件都可以直接访问到里面的内容,而不用通过中间组件逐级传递,就像一个全局变量一样。

在 App -> Toolbar -> ThemedButton 使用 props 属性传递 theme,Toolbar 作为中间组件将 theme 从 App 组件 传递给 ThemedButton 组件。

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  // Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
  // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
  // 因为必须将这个值层层传递所有组件。
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}

使用 context,就可以避免通过中间元素传递 props 了

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

虽然解决了状态传递的问题却引入了 2 个新的问题。

1. 我们引入的 context 就像全局变量一样,里面的数据可以被子组件随意更改,可能会导致程序不可预测的运行。

2. context 极大地增强了组件之间的耦合性,使得组件的复用性变差,比如 ThemedButton 组件因为依赖了 context 的数据导致复用性变差。

我们知道,redux 不正是提供了管理共享状态的能力嘛,我们只要通过 redux 来管理 context 就可以啦,第一个问题就可以解决了。

 

Provider 组件

React-Redux 提供 Provider 组件,利用了 react 的 context 特性,将 store 放在了 context 里面,使得该组件下面的所有组件都能直接访问到 store。大致实现如下:

class Provider extends Component {
  // getChildContext 这个方法就是设置 context 的过程,它返回的对象就是 context,所有的子组件都可以访问到这个对象
  getChildContext() {
    return {
      store: this.props.store
    };
  }
  render() {
    return this.props.children;
  }
}

Provider.childContextTypes = {
  store: React.PropTypes.object
}

那么我们可以这么使用,将 Provider 组件作为根组件将我们的应用包裹起来,那么整个应用的组件都可以访问到里面的数据了

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

import { Provider } from 'react-redux'; import { createStore } from 'redux'; import todoApp from './reducers'; import App from './components/App'; const store = createStore(todoApp); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )

 

展示(Dumb Components)组件和容器(Smart Components)组件

还记得我们的第二个问题吗?组件因为 context 的侵入而变得不可复用。React-Redux 为了解决这个问题,将所有组件分成两大类:展示组件和容器组件。

展示组件

展示组件有几个特征

1. 组件只负责 UI 的展示,没有任何业务逻辑

2. 组件没有状态,即不使用 this.state

3. 组件的数据只由 props 决定

4. 组件不使用任何 Redux 的 API

 

展示组件就和纯函数一样,返回结果只依赖于它的参数,并且在执行过程里面没有副作用,让人觉得非常的靠谱,可以放心的使用。

import React, { Component } from 'react';
import PropTypes from 'prop-types';

class Title extends Component {
  static propTypes = {
    title: PropTypes.string
  }

  render () {
    return (
      <h1>{ this.props.title }</h1>
    )
  }
}

像这个 Title 组件就是一个展示组件,组件的结果完全由外部传入的 title 属性决定。

容器组件

容器组件的特征则相反

1. 组件负责管理数据和业务逻辑,不负责 UI 展示

2. 组件带有内部状态

3. 组件的数据从 Redux state 获取

4. 使用 Redux 的 API

 

你可以直接使用 store.subscribe() 来手写容器组件,但是不建议这么做,因为这样无法使用 React-redux 带来的性能优化。

React-redux 规定,所有的展示组件都由用户提供,容器组件则是由 React-Redux 的 connect() 自动生成。

 

高阶组件 Connect 

React-redux 提供 connect 方法,可以将我们定义的展示组件生成容器组件。connect 函数接受一个展示组件参数,最后会返回另一个容器组件回来。所以 connect 其实是一个高阶组件(高阶组件就是一个函数,传给它一个组件,它返回一个新的组件)。

import { connect } from 'react-redux';
import Header from '../components/Header';

export default connect()(Header);

上面代码中,Header 就是一个展示组件,经过 connect 处理后变成了容器组件,最后把它导出成模块。这个容器组件没有定义任何的业务逻辑,所有不能做任何事情。我们可以通过 mapStateToProps 和 mapDispatchToProps 来定义我们的业务逻辑。

import { connect } from 'react-redux';
import Title from '../components/Title';

const mapStateToProps = (state) => {
  return {
    title: state.title
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onChangeColor: (color) => {
      dispatch({ type: 'CHANGE_COLOR', color });
    }
  }
}

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

mapStateToProps 告诉 connect 我们要取 state 里的 title 数据,最终 title 数据会以 props 的方式传入 Title 这个展示组件。

mapStateToProps 会订阅 Store,每当 state 更新的时候,就会自动执行,重新计算展示组件的参数,从而触发展示组件的重新渲染。

mapDispatchToProps 告诉 connect 我们需要 dispatch action,最终 onChangeColor 会以 props 回调函数的方式传入 Title 这个展示组件。

 

Connect 组件大概的实现如下

export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    constructor () {
      super()
      this.state = {
        allProps: {}
      }
    }

    componentWillMount () {
      const { store } = this.context
      this._updateProps()
      store.subscribe(() => this._updateProps())
    }

    _updateProps () {
      const { store } = this.context
      let stateProps = mapStateToProps
        ? mapStateToProps(store.getState(), this.props) // 将 Store 的 state 和容器组件的 state 传入 mapStateToProps
        : {} // 判断 mapStateToProps 是否传入
      let dispatchProps = mapDispatchToProps
        ? mapDispatchToProps(store.dispatch, this.props) // 将 dispatch 方法和容器组件的 state 传入 mapDispatchToProps
        : {} // 判断 mapDispatchToProps 是否传入
      this.setState({
        allProps: {
          ...stateProps,
          ...dispatchProps,
          ...this.props
        }
      })
    }

    render () {
      // 将 state.allProps 展开以容器组件的 props 传入
      return <WrappedComponent {...this.state.allProps} />
    }
  }
  return Connect
}

 

小结

至此,我们就很清楚了,原来 React-redux 就是通过 Context 结合 Redux 来实现 React 应用的状态管理,通过 Connect 这个高阶组件来实现展示组件和容器组件的连接的。

 

更多精彩内容,欢迎关注微信公众号~

 

 
 
 
posted @ 2020-03-14 22:39  阿林十一  阅读(449)  评论(0编辑  收藏  举报