Redux 02 高阶组件

Redux 02 高阶组件

函数式编程yyds

在连接ReduxReact之前,有一个非常重要的思想,就是容器型组件和展示型组件,这个思想最早来源于这篇文章
值的说的是,这篇文章的开头,作者在2019年更新了最新的内容,提到在hook出现之后,这么区分就意义不大了,这篇文章已经成为时代的眼泪。确实在hook出现之后,基本上也宣判了高阶组件的死刑。不管如何,目前我们在使用Redux,高阶组件也是旧React的主要概念之一,也是函数式编程理念的体现,还是要来看一看。至于hook,以后要另找视频学习。

容器型组件和展示型组件

那篇文章里讲了,把React的组件可以划分为这两种,特点分别如下:

展示型组件

  1. 集中于外观和展示数据
  2. 包含展示型和容器型组件,内部有DOM元素和自己的样式
  3. 通常允许使用this.props.children
  4. 对应用的其他部分没有依赖,只根据传入的数据展示结果
  5. 不关心数据如何载入和变化
  6. 只通过props接收数据和函数
  7. 无状态组件,没有自己的state(或者只有和样式而不是数据相关的state
  8. 以普通组件的形式存在

容器型组件

  1. 集中于功能如何实现
  2. 包含展示型组件和容器型组件,代码内部除了包裹用的div之外,没有DOM元素,也没有自己的样式
  3. 向其他组件提供数据和行为(函数)
  4. 负责发送Flux(Redux)的行为,负责把这些行为提供给展示型组件使用
  5. 有状态组件,连接到集中管理的状态库
  6. 通常以高阶组件的形式存在,对于Redux,使用connect()函数生成

高阶组件

何谓高阶组件,就是接受一个组件并且返回一个组件的组件。在React中组件=函数,所以高阶组件就是高阶函数,高阶函数是至少满足下列一个条件的函数:

  1. 接受一个或多个函数作为输入
  2. 输出一个函数。

React的高阶组件是接受一个函数返回一个函数的函数,同时满足高阶函数的两个条件,在函数式编程中,返回另一个函数的高阶函数被称为Curry化的函数。

高阶组件简单的说,就是可以增强原来组件的功能,加上一些额外数据或者交互属性。

修改组件结构

在上一篇文章中,组件结构如下:

<div className="App">
    <button onClick={this.handleIncrement}>+1</button>
    <button>-1</button>
    <h2>{this.state.current}</h2>
</div>

显然还比较low,因为两个功能按钮和显示自增自减结果的h2标题共同完成一个功能,我们把它们放到同一个组件ChangeValue里去,再定义新组件MyButtonMyResult

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的话,其实可以把功能也放在高阶组件里传入。接下来就可以继续看ReduxReact的连接了。

LICENSED UNDER CC BY-NC-SA 4.0
Comment