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"": ['transform-decorators-legacy']
}
有些项目配置不用.babelrc,上面方法不适用,但关键是不变的。安装babel-plugin-transform-decorators-legacy,跟着在babel配置增加decorator插件就可以
6、总结
一个简短的@connect()包含装饰器、高阶组件好几个知识点,要弄明白需要花点时间 ,但是把知识点整理下来还是收获很多,下次见到不会懵逼啦!!!