JTangming / blog

My repository on GitHub.
Other
53 stars 0 forks source link

理解 react-redux 之 connect #8

Open JTangming opened 7 years ago

JTangming commented 7 years ago

redux 看上去足够简单,但却有非常强的规范约束,作为 react 全家桶中极为重要的组成部分,是 JavaScript 应用程序的可预测状态容器,是一种状态管理的解决方案。

来看其官网上的一个 gist demo:
import { createStore } from 'redux'

function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1
  case 'DECREMENT':
    return state - 1
  default:
    return state
  }
}

let store = createStore(counter)

store.subscribe(() =>
  console.log(store.getState())
)

store.dispatch({ type: 'INCREMENT' }) // 1
store.dispatch({ type: 'INCREMENT' }) // 2
store.dispatch({ type: 'DECREMENT' }) // 1

redux 的使用规范是确保整个应用程序的数据状态存储在单个 store 的对象树中,在 reducer 中更改状态树 state ,唯一方法是 dispatch action,这个 dispatch action 包含 type 及相关变化数据的一个对象。

一个应用往往有复杂的组件嵌套,单纯使用 redux的话,可以在最外层容器组件中初始化 store,通用做法是将 state 上的属性作为 props 层层传递下去,这种实践想想都是令人厌烦的。那接下来我们就引入 react-redux 的 connect。

react-redux 两个主要的 API 是 Provider、connect,我们先从一个简单的例子开始:

// root.js
class Root extends React.Component {
  _store: Store<any>;
  render() {
    return (
      <Provider store={this._store}>
        <App />
      </Provider>
    );
  }
}

// app.js
class App extends React.Component {}

const mapStateToProps = state => {
  return {nav: state.router};
};

export default connect(mapStateToProps)(App);

例子通过 connect 的方式将 store 数据连接到组件中,在 state 变化的时候,组件的 mapStateToProps 会被调用并重新计算出一个新的 stateProps 来更新当前组件数据。connect 做了两件事情,一是把组件需要的 store 对象树属性 map 到当前组件中,二是把组件需要的数据结构在这个 map 函数中以 plain Object 返回,这做可以方便的定制当前组件需要的数据从而避免了层层 store 数据的嵌套。

要理解 connect 是如何工作的,需要先理解以下两点:

理解 react 的 context

旧版 context 的一个简单使用例子如下:

// 顶层组件
class Root extends Component {
    getChildContext() {
        return {
            text: 'xxx'
        }
    }
    render() {
        return <Parent />
    }
}

// 中间层组件
class IntermediateC extends Component {
    render() {
        return <Child />
    }
}

// 需要数据的组件
class Child extends Component {
    render() {
        return this.context.text
    }
}

旧版 context 不用在每个中间层组件中都显示地将 props 设置到子组件的属性中,通过顶层组件中的 getChildContext() 方法设置需要返回的数据即可。

新版 context 用 Provider、Consumer 对来实现,Provider 用来包裹顶层组件,Consumer 用来包裹底层获取数据的组件。但是获取数据的组件有多个数据来源,那么以上的嵌套地域又将重现。关于新旧版的 context API 在使用上有很大区别,但本质上都是解决同样的问题,即解决跨组件数据传递的问题,只是新版 context API 更符合 react 风格。

具体可以参见 https://reactjs.org/docs/context.html

Provider 的工作原理

Provider 查看源码其提供了3个方法:getChildContext、constructor、render,其中构造函数获取 props.store 供组件内部使用,render 方法返回一个 react 子元素,源码为 return Children.only(this.props.children),其中的 Children.only 是 react 提供的方法,this.props.children 表示的是一个 Provider 的子组件。

Provider 是一个容器组件, 从源码看做的事情很简单,即把嵌套的内容原封不动地作为子组件给渲染出来,最重要的是把 props.store 放到 context,从而子组件 connect 的时候都可以获取到。

connect 底层工作原理

要从 connect 源码看的确复杂了,这里不再逐行解析源码。我们来继续看看 connect 的使用: connect(...args)(Component)

通过在根组件 Provider 组件设置 store 作为整个顶层组件的 context,其下所有子孙节点组件都可以获取到这个 store 对象树的数据。那接下来 connect 做的事情其实就很明了了,即 connect 首先执行的是一个 HOC,在这个高阶组件中,connect 接下来把 mapStateToProps 和 mapDispatchToProps 里的返回的属性,连同通过 context 获取到的 store 一起,过滤包装 store 数据最终传递给了被包裹的组件,connect 不会修改传递给它的组件类,相反它返回一个新的、被连接的组件类供开发者使用。

最后 connect 通过 redux store 的 subscribe API 来监听数据的变化,通过 shallowEqual 对比之前组件缓存的 props 和新计算出的属性,来决定是否需要更新组件,即重新将 args 里边的 props 传递给第二个被传入的 Component,达到更新组件的目的。理解了 connect 的工作原理,那么接下来就知道在开发过程中需要注意什么了。

reference