@connect() 看不懂怎么办

2,502 阅读6分钟
作者:Richar

1、背景

@connect(({ appMod }) => ({
  appMod
}))
...
export default App

在antd pro项目中,经常碰到@connect会感到陌生懵逼,直觉告诉我它不是一个好东西,它是干嘛?它是什么语法?为什么它可以用来获取数据?在一连串问号中,我尝试去剖析它。在剖析前,需要学习两个知识点。

2、装饰器(Decorator)

装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。 装饰器是一种函数,写成@ + 函数名。它可以放在类和类方法的定义前面。

@frozen class Foo {
  @configurable(false)
  @enumerable(true)
  method() {}
  @throttle(500)
  expensiveMethod() {}
}

上面是《ESCAScript 6 入门》关于装饰器的介绍,它可以装饰类和类的方法,这里只关注装饰类的情况,下面看一个例子

@addCxh
class DaBao {}
function addCxh(target){
    target.cxh = "first"
}
console.log(DaBao.cxh); // first

从打印结果确定,装饰器给类添加了一个属性cxh,@+函数是固定的语法,装饰函数可以获取到类DaBao;通俗来说,装饰器就是给类添加属性和方法 明白装饰器之后,@connect()即给类组件添加一个connect方法,解决疑惑点一个

3、高阶组件

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。 具体而言,高阶组件是参数为组件,返回值为新组件的函数。 下面看一个简单的高阶组件:

import React, { Component } from 'react'
export default (WrappedComponent) => {
  class NewComponent extends Component {
    // 可以做很多自定义逻辑
    render () {
      return <WrappedComponent />
    }
  }
  return NewComponent
}

这个例子没做什么特别操作,只是构建一个新的组件类NewComponent,然后传入WrappedComponent渲染出来,但是我们可以给NewComponent加一些数据启动工作:

import React, { Component } from 'react'
export default (WrappedComponent, name) => {
  class NewComponent extends Component {
    constructor () {
      super()
      this.state = { data: null }
    }
    componentWillMount () {
      let data = localStorage.getItem(name)
      this.setState({ data })
    }
    render () {
      return <WrappedComponent data={this.state.data} />
    }
  }
  return NewComponent
}

现在NewComponent会在componentWillMount生命周期根据name从localStorage中获取数据,通过setState保存数据到state.data中,在渲染的时候将state.data通过props.data传递给WrappedComponent 那高阶组件有什么作用呢?假设上面的代码在src/wrapWithLoadData.js 文件中的,我们可以在别的地方这么用它:

import wrapWithLoadData from './wrapWithLoadData'
class InputWithUserName extends Component {
  render () {
    return <input value={this.props.data} />
  }
}
InputWithUserName = wrapWithLoadData(InputWithUserName, 'username')
export default InputWithUserName
  • 假如InputWithUserName的功能需求是挂载的时候从LocalStorage里面加载username字段作为input的value值。现在有了wrapWithLoadData,我们可以很容易地做到这件事。
  • 只需要定义一个非常简单的InputWithUserName,它会把props.data作为input的value值。然把这个组件和'username'传给wrapWithLoadData,wrapWithLoadData会返回一个新的组件,我们用这个新的组件覆盖原来的InputWithuserName,然后再导出模块 最后使用InputWithUserName组件是这样的
import InputWithUserName from './InputWithUserName'
class Index extends Component {
  render () {
    return (
      <div>
        用户名:<InputWithUserName />
      </div>
    )
  }
}

WrapWithLoadData在组件挂载的时候先去LocalStorage加载数据,渲染的时候再通过props.data传给真正的InputWithUserName 如果现在需要一个文本输入框组件,它也需要LocalStorage加载'content'字段的数据。我们只需要定义一个新的TextareaWithContent:

import wrapWithLoadData from './wrapWithLoadData'
class TextareaWithContent extends Component {
  render () {
    return <textarea value={this.props.data} />
  }
}
TextareaWithContent = wrapWithLoadData(TextareaWithContent, 'content')
export default TextareaWithContent

写起来非常简单,根本不需要重复写LocalStorage加载数据的逻辑,直接用WrapWithLoadData包装一下就可以

我们来回顾一下到底发生了什么事情,对于 InputWithUserName 和 TextareaWithContent 这两个组件来说,它们的需求有着这么一个相同的逻辑 :“挂载阶段从 LocalStorage 中加载特定字段数据”。

  • 如果按照之前的做法,我们需要给它们两个都加上 componentWillMount 生命周期,然后在里面调用 LocalStorage。
  • 要是有第三个组件也有这样的加载逻辑,我又得写一遍这样的逻辑。但有了 wrapWithLoadData 高阶组件,我们把这样的逻辑用一个组件包裹了起来,并且通过给高阶组件传入 name 来达到不同字段的数据加载。充分复用了逻辑代码。

到这里,高阶组件的作用其实不言而喻,其实就是为了组件之间的代码复用。组件可能有着某些相同的逻辑,把这些逻辑抽离出来,放到高阶组件中进行复用。高阶组件内部的包装组件和被包装组件之间通过 props 传递数据。

4、connect

  • 学习完前面两个知识点后,我们知道@connect()的作用是给组件类添加一个connect方法并执行它;
  • connect方法来自dva,查看源码dva的connect引用react-redux的connect,实际就是redux的connect;它的作用是连接React组件与Redux store。下面看看connect具体实现。 由于connect的源码很长,我们只看主要逻辑(这是旧的代码,最新代码已经用Hooks重写)
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
  return function wrapWithConnect(WrappedComponent) {
    class Connect extends Component {
      constructor(props, context) {
        // 从祖先Component处获得store
        this.store = props.store || context.store
        this.stateProps = computeStateProps(this.store, props)
        this.dispatchProps = computeDispatchProps(this.store, props)
        this.state = { storeState: null }
        // 对stateProps、dispatchProps、parentProps进行合并
        this.updateState()
      }
      shouldComponentUpdate(nextProps, nextState) {
        // 进行判断,当数据发生改变时,Component重新渲染
        if (propsChanged || mapStateProducedChange || dispatchPropsChanged) {
          this.updateState(nextProps)
            return true
          }
        }
        componentDidMount() {
          // 改变Component的state
          this.store.subscribe(() = {
            this.setState({
              storeState: this.store.getState()
            })
          })
        }
        render() {
          // 生成包裹组件Connect
          return (
            <WrappedComponent {...this.nextState} />
          )
        }
      }
      Connect.contextTypes = {
        store: storeShape
      }
      return Connect;
    }
  }

从源码可以看出,

  • 1、connect是一个高阶函数
  • 2、传入MapStateToProps,mapDispatchToProps,返回一个生产Component的函数(wrapWithConnect)
  • 3、然后再将真正的Component作为参数传入wrapWithConnect,这样就产出一个经过Connect包裹的组件,此组件通过props.store获取祖先Component的store props包括stateProps、dispathchProps、parentProps,合并在一起得到nextState,作为props传给真正的Component
  • 4、componentDidMount时,添加事件this.store.subscribe(this.handleChange),实现页面交互shouldComponentUpdate时判断是否有避免进行渲染,提升页面性能,并得到nextState componentWillUnmount时移除注册事件this.handlechange

根据connect定义,调用connect的时候应该是这样的:

const mapStateToProps = (state) => {
    return {
    appMode: state.appMode
  }
}
const mapDispatchToProps = (dispatch, ownProps) => {
    return {
    xxx: () => {}
  }
}
connect(mapStateToProps,mapDispatchToProps,mergeProps,options)(App)

将connect调用方法与上面的代码进行对比

@connect(({ appMod }) => ({
  appMod
}))
...
export default App

装饰器把App作为参数传入connect方法,使上下两种connect的调用效果相同,mapStateToProps、mapDispatchToProps依然写在connect调用括号内,

({appMod}) => ({appMod})

把appMode传入给App组件,下面写法代码更为简洁。 装饰器可以理解为是一种语法糖,使用代码变得更加简洁。使用它有门槛,需要了解ES7装饰器的语法,在项目还要配置一个babel插件,因为很多浏览器还不支持

5、配置decorator插件

安装依赖:

npm install babel-plugin-transform-decorators-legacy --save-dev

修改babel配置 .babelrc:

{
    // 新增一个decorator 插件
    "plugins&quot": ['transform-decorators-legacy']
}

有些项目配置不用.babelrc,上面方法不适用,但关键是不变的。安装babel-plugin-transform-decorators-legacy,跟着在babel配置增加decorator插件就可以

6、总结

一个简短的@connect()包含装饰器、高阶组件好几个知识点,要弄明白需要花点时间 ,但是把知识点整理下来还是收获很多,下次见到不会懵逼啦!!!