在连接Redux
和React
之前,有一个非常重要的思想,就是容器型组件和展示型组件,这个思想最早来源于这篇文章。
值的说的是,这篇文章的开头,作者在2019年更新了最新的内容,提到在hook
出现之后,这么区分就意义不大了,这篇文章已经成为时代的眼泪。确实在hook
出现之后,基本上也宣判了高阶组件的死刑。不管如何,目前我们在使用Redux
,高阶组件也是旧React的主要概念之一,也是函数式编程理念的体现,还是要来看一看。至于hook
,以后要另找视频学习。
容器型组件和展示型组件
那篇文章里讲了,把React的组件可以划分为这两种,特点分别如下:
展示型组件
- 集中于外观和展示数据
- 包含展示型和容器型组件,内部有
DOM
元素和自己的样式 - 通常允许使用
this.props.children
- 对应用的其他部分没有依赖,只根据传入的数据展示结果
- 不关心数据如何载入和变化
- 只通过
props
接收数据和函数 - 无状态组件,没有自己的
state
(或者只有和样式而不是数据相关的state
) - 以普通组件的形式存在
容器型组件
- 集中于功能如何实现
- 包含展示型组件和容器型组件,代码内部除了包裹用的
div
之外,没有DOM
元素,也没有自己的样式 - 向其他组件提供数据和行为(函数)
- 负责发送
Flux
(Redux
)的行为,负责把这些行为提供给展示型组件使用 - 有状态组件,连接到集中管理的状态库
- 通常以高阶组件的形式存在,对于
Redux
,使用connect()
函数生成
高阶组件
何谓高阶组件,就是接受一个组件并且返回一个组件的组件。在React
中组件=
函数,所以高阶组件就是高阶函数,高阶函数是至少满足下列一个条件的函数:
- 接受一个或多个函数作为输入
- 输出一个函数。
React
的高阶组件是接受一个函数返回一个函数的函数,同时满足高阶函数的两个条件,在函数式编程中,返回另一个函数的高阶函数被称为Curry
化的函数。
高阶组件简单的说,就是可以增强原来组件的功能,加上一些额外数据或者交互属性。
修改组件结构
在上一篇文章中,组件结构如下:
<div className="App">
<button onClick={this.handleIncrement}>+1</button>
<button>-1</button>
<h2>{this.state.current}</h2>
</div>
显然还比较low
,因为两个功能按钮和显示自增自减结果的h2
标题共同完成一个功能,我们把它们放到同一个组件ChangeValue
里去,再定义新组件MyButton
和MyResult
。
ChangeValue
组件的代码如下:
import React, {Component} from 'react';
import {store} from "../index";
import {selfDecrement, selfIncrement} from "../action/actionBuilder";
import MyButton from "./MyButton";
import MyResult from "./MyResult";
class ChangeValue extends Component {
constructor(props) {
super(props);
this.state = {
current: store.getState().value
}
}
handleIncrement = (e) => {
e.preventDefault();
store.dispatch(selfIncrement(1));
this.setState({current: store.getState().value});
}
handleDecrement = (e) =>{
e.preventDefault();
store.dispatch(selfDecrement(1));
this.setState({current: store.getState().value});
}
render() {
return (
<div className="App">
<MyStyledButton onClick={this.handleIncrement}/>
<MyButton onClick={this.handleDecrement}>-1</MyButton>
<MyResult value={this.state.current}/>
</div>
);
}
}
export default ChangeValue;
这其中使用到的MyButton
组件如下:
import React, {Component} from 'react';
class MyButton extends Component {
constructor(props) {
super(props);
}
render() {
return (
<button {...this.props}>+1</button>
);
}
}
export default MyButton;
MyResult
组件的代码如下:
import React, {Component} from 'react';
class MyResult extends Component {
constructor(props) {
super(props);
}
render() {
return (
<h2>{this.props.value}</h2>
);
}
}
export default MyResult;
重新组织结构之后看上去舒服多了,不过依然还是比较初级的代码,非常简单。主要是为了玩一下高阶函数。这里有一个注意的地方,就是给组件传递这种驼峰属性的时候,组件并不会有onClick
的效果,而是一定要把onClick
最终传递到dom
属性上(小写开头的元素)才有最终的作用,所以这里可以直接给MyButton
传递onClick
,并最终把onClick传递到button
元素上即可。
写一个没有任何功能的高阶组件
先来写一个最基本的高阶组件的伪代码看一下:
const Wrapper = (WrappedComponent) =>{
working......
return OneComponent;
}
这个严格按照高阶组件的要求,接受一个组件,返回一个组件,我们把这个东西改成实际的代码,比如这个组件接受什么组件就原样返回这个组件:
// Wrapper.js
const Wrapper = (WrappedComponent) => {
return WrappedComponent;
};
export default Wrapper;
高阶组件依然是一个组件,我们用这个组件来替换原来的MyButton
试试:
// ChangeValue.js
......
import MyButton from "./MyButton";
import Wrapper from "./Wrapper";
class ChangeValue extends Component {
render() {
const WrappedButton = Wrapper(MyButton);
return (
<div className="App">
<WrappedButton onClick={this.handleIncrement} />
<WrappedButton onClick={this.handleDecrement} />
<MyResult value={this.state.current}/>
</div>
);
}
}
export default ChangeValue;
之后发现,一切照旧,还能工作,看似给高阶组件传入的onClick
属性,最后也神奇般的传递给了最终的button
元素,这是怎么做到的呢?
高阶组件的工作原理剖析
其实高阶组件的工作原理,就和柯里化的函数是一样的。
先看这句:const WrappedButton = Wrapper(MyButton);
执行之后,高阶组件里什么都没写,直接返回传入的组件,那么此时WrappedButton
就是MyButton
组件,所以onClick
实际上传递给了MyButton
组件。
我们已经知道,组件其实在内部,会被React.render()
函数来执行,通过给组件函数传入props
,执行之后得到dom
元素,经过比较之后最终挂载上去,也就是所有的组件函数都会被执行。
我们现在的MyButton
组件是类形态的组件,如果将其更改为函数组件,可以看的更加明白一些:
//MyButton.js
const MyButton = (props) => (
<button {...props}>+1</button>
);
我们现在假设高阶组件传入了MyButton
,返回MyButton
,实际的高阶组件的代码如下:
const Wrapper = (MyButton) => {
// 本来应该是 return MyButton;
// 把MyButton用MyButton.js中的代码替换
return (props) => (
<button {...props}>+1</button>
)
};
export default Wrapper;
当然,此时两个按钮都会显示+1
,但功能还是一个自增一个自减,现在不考虑都显示+1
。此时我们来看,传递给高阶组件的props
,其实就是return
的那个函数的props
,一般来说应该透传所有的props
,如果高阶组件想在这里做手脚,那么就要在这里操作了。
然后我们可以把button
元素换成MyButton
组件,因为加上尖括号<>
之后的组件,等同于dom
元素,而MyButton
就是一个button
元素。
由于这是以MyButton
为例,将MyButton
替换成普通的参数名称,我们得到了这个无功能高阶组件的标准形态。
标准的高阶组件形态
有三种形态,本质上相同。
标准形态
const StandardWrapper= (WrappedComponent) => {
return (props) => (
<WrappedComponent {...props} />
);
};
export default StandardWrapper;
由于返回的是一个函数,可以继续写成箭头函数,高阶组件还可以写成如下形态:
连续箭头函数形态
const Wrapper = (WrappedComponent) => (WrappedComponentProps) => <WrappedComponent {...WrappedComponentProps}/>;
用这个高阶组件替换原来的组件之后,传递给原来组件的所有props
依然能够透传到原来组件,而高阶组件中,也可以操作这些props
,这就达成了我们的目标。
返回类组件形态
函数型组件也可以用类组件来进行替换,另外一种形态的高阶组件是:
import React from "react";
const StandardWrapper = (WrappedComponent) => {
return class Result extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<WrappedComponent {...this.props} />
);
}
}
};
export default StandardWrapper;
虽然外表不同,但其本质都是柯里化函数,可以在每一层函数中,操作要传给返回的组件的props
变量,从而影响最终组件的效果。
写几个高阶组件玩玩
现在我们的MyButton
组件有一个问题,就是显示的内容+1-1
写死了,导致不太好处理,我们应该弄一个事件和显示结果都根据props
来显示的按钮,如下:
//MyButton.js
const MyButton = (props) => (
<button {...props}>{props.text}</button>
);
export default MyButton;
然后我们就可以把自增自减的逻辑,显示什么名称之类的放在高阶组件中,只要包装一下MyButton,就得到我们想要的结果。
控制显示内容
高阶组件在其中,修改text
为+1
或者-1
:
const SelfIncrementButton = (WrappedComponent) => {
return (props) => (
<WrappedComponent {...props} text={"+1"}/>
);
};
const SelfDerementButton = (WrappedComponent) =>{
return (props) => (
<WrappedComponent {...props} text={"-1"}/>
);
}
由于目前是不完美的结合了redux
,所以依然要给高阶组件传入onClick
属性:
///ChangeValue.render()
render() {
const IncreButton = SelfIncrementButton(MyButton);
const DecreButton = SelfDerementButton(MyButton);
return (
<div className="App">
<IncreButton onClick={this.handleIncrement}/>
<DecreButton onClick={this.handleDecrement}/>
<MyResult value={this.state.current}/>
</div>
);
}
改个样式,显示自增自减
由于可以随意控制props,就方便多了:
const StyledIncrementButton = (WrappedComponent) => (props) => {
const style = {backgroundColor: "blue", color: "white", borderRadius: 5}
return <WrappedComponent {...props} text={"自增"} style={style}/>;
}
这个高阶组件把text
属性设置为“自增”两个字,然后加上一些样式,传递给WrappedComponent
,让原来没有样式的组件额外加上了样式:
///ChangeValue.render()
render() {
const StyledButton = StyledIncrementButton(MyButton);
const DecreButton = SelfDerementButton(MyButton);
return (
<div className="App">
<StyledButton onClick={this.handleIncrement}/>
<DecreButton onClick={this.handleDecrement}/>
<MyResult value={this.state.current}/>
</div>
);
}
一些变化
由于高阶组件是自己编写的,只要能接受组件返回组件即可,所以也可以接受一些额外的参数,比如直接传入颜色参数:
const StyledIncrementButton = (WrappedComponent, color="blue") => (props) => {
const style = {backgroundColor: color, color: "white", borderRadius: 5}
return <WrappedComponent {...props} text={"自增"} style={style}/>;
}
const StyledDecrementButton = (WrappedComponent, size=16) => (props) => {
const style = {backgroundColor: "blue", color: "white", borderRadius: 5, fontSize: size}
return <WrappedComponent {...props} text={"自减"} style={style}/>;
}
然后可以根据参数来控制颜色或者字体:
///ChangeValue.render()
render() {
const StyledButton1 = StyledIncrementButton(MyButton);
const StyledButton2 = StyledIncrementButton(MyButton,"green");
const DecreButton1 = StyledDecrementButton(MyButton);
const DecreButton2 = StyledDecrementButton(MyButton,30);
return (
<div className="App">
<StyledButton1 onClick={this.handleIncrement}/>
<StyledButton2 onClick={this.handleIncrement}/>
<DecreButton1 onClick={this.handleDecrement}/>
<DecreButton2 onClick={this.handleDecrement}/>
<MyResult value={this.state.current}/>
</div>
);
}
去掉store
为了方便,把使用store
的代码去掉,可以编写自定义的高阶组件:
const CustomButton = (WrappedComponent, behavior = null, text = "按钮", style = {}) => (props) =>
<WrappedComponent {...props} style={style} text={text} onClick={behavior}/>;
此时的ChangeValue.js
可以写成如下:
import React, {Component} from 'react';
import MyButton from "./MyButton";
import MyResult from "./MyResult";
class ChangeValue extends Component {
constructor(props) {
super(props);
this.state = {
current: 1,
}
}
handleIncrement = (e) => {
e.preventDefault();
this.setState((state, props) => {
return {
current: state.current + 1
}
});
}
handleDecrement = (e) => {
e.preventDefault();
this.setState((state, props) => {
return {
current: state.current - 1
}
});
}
render() {
const InButton = CustomButton(MyButton, this.handleIncrement, "自增1", {
backgroundColor: "orange",
fontSize: 16,
color: 'white',
borderRadius: 10,
border: "none"
});
const DeButton = CustomButton(MyButton, this.handleDecrement, "自减1");
return (
<div className="App">
<InButton onClick={this.handleIncrement}/>
<DeButton onClick={this.handleDecrement}/>
<MyResult value={this.state.current}/>
</div>
);
}
}
这个按钮就可以根据传入的行为样式和显示文字来自定义了。
写了这么多,终于搞定了高阶组件。如果说不需要结合Redux
的话,其实可以把功能也放在高阶组件里传入。接下来就可以继续看Redux
和React
的连接了。