# React组件化

# 一、组件跨层级通信(Context)

React中使用Content实现祖代组件向后代组件跨层级传值。在Content下有两个角色:

  • Provider:外层提供数据的组件
  • Consumer:内层获取数据的组件

使用Content:(1)创建Content;(2)获取Provider和Consumer;(3)Provider提供值,Consumer获取值。

范例:模拟Redux存放全局状态

import React, { Component } from 'react'

// 1、创建上下文
const Content = React.createContext();
// 2、获取Provider、Consumer
const { Provider, Consumer } = Content;

function Child(props) {
    return (
        <div onClick={() => props.add()}>
            {props.counter}
        </div>
    )
}

export default class ContentText extends Component {
    // state是要传递的数据
    state = { counter: 0 };
    // add方法可以修改状态
    add = () => {
        this.setState(nextState => ({ counter: nextState.counter + 1 }));
    };
    render() {
        return (
            <div>
                <Provider value={{ counter: this.state.counter, add: this.add }}>
                    {/* Consumer中内嵌函数,其参数是传递的数据,返回要渲染的组件 */}
                    {/* 把value展开传递给Child */}
                    <Consumer>{value => <Child {...value} />}</Consumer>
                    <Consumer>{value => <Child {...value} />}</Consumer>
                </Provider>
            </div>
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 二、高阶组件(HOC)

高阶组件是一个工厂函数,它接收一个组件并返回另一个组件。例子

import React from "react";

// Lesson保证功能单一,它不关心数据来源,只负责显示
function Lesson(props) {
    return (
        <div>
            {props.stage} - {props.title}
        </div>
    );
}
// 模拟数据
const lessons = [
    { stage: "React", title: "核心API" },
    { stage: "React", title: "组件化1" },
    { stage: "React", title: "组件化2" }
];
// 定义高阶组件withContent,负责包装传入组件Comp,包装后组件能够根据传入索引获取课程数据,真实案例中可以通过api查询得到。会返回包装后的组件。
const withContent = Comp => props => {
    const content = lessons[props.idx];
    // {...props}将属性展开传递下去
    return <Comp {...content} />;
};
// LessonWithContent是Lesson经过包装后的组件
const LessonWithContent = withContent(Lesson);

export default function HocTest() {
    // HocTest渲染三个LessonWithContent组件
    return (
        <div>
            {[0, 1, 2].map((item, idx) => (
                <LessonWithContent idx={idx} key={idx} />
            ))}
        </div>
    );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 链式调用

高阶组件最巧妙的一点,可以链式调用。

// 高阶组件withLog负责包装传入组件Comp
// 包装后组件在挂载时可以输出日志记录
const withLog = Comp => {
    // 返回组件需要生命周期,因此声明为class组件
    return class extends React.Component {
        render() {
            return <Comp {...this.props} />;
        }
        componentDidMount() {
            console.log("didMount", this.props);
        }
    };
};
// LessonWithLog是包装后的组件,既能够根据传入索引获取课程数据又可以输出日志
const LessonWithLog = withLog(withContent(Lesson));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 装饰器写法

CRA项目中默认不支持js代码使用装饰器语法,可修改后缀名为tsx则可以直接支持

// 装饰器只能用在class上,执行顺序从下往上
@withLog
@withContent
class Lesson2 extends React.Component {
    render() {
        return (
            <div>
                {this.props.stage} - {this.props.title}
            </div>
        );
    }
}
export default function HocTest() {
    return (
        <div>
            {[0, 0, 0].map((item, idx) => (
                <Lesson2 idx={idx} key={idx} />
            ))}
        </div>
    );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 三、组件复合(Composition)

复合组件给与你足够的敏捷去定义自定义组件的外观和行为。

# 组件复合(类似于插槽)

范例:Dialog组件负责展示,内容从外部传入,核心是利用props.children获取标签里面的内容。

import React from "react";

// Dialog定义组件外观和行为
function Dialog(props) {
    // props.children就代表了<Dialog>标签内部的内容
    return <div style={{ border: "1px solid blue" }}>{props.children}</div>;
}
export default function Composition() {
    return (
        <div>
            {/* 传入显示内容 */}
            <Dialog>
                <h1>组件复合</h1>
                <p>复合组件给与你足够的敏捷去定义自定义组件的外观和行为</p>
            </Dialog>
        </div>
    );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 具名插槽

上面的props.children其实就是合法的js表达式。利用 props.children.def获取指定插槽。

import React from "react";

// Dialog定义组件外观和行为
function Dialog(props) {
    return (
        <div style={{ border: "1px solid blue" }}>
            <div>{props.children.def}</div>
            <div>{props.children.foot}</div>
        </div>);
}
export default function Composition() {
    return (
        <div>
            {/* 传入显示内容 */}
            <Dialog>
                {{
                    def: (
                        <div>
                            <h1>组件复合</h1>
                            <p>复合组件给与你足够的敏捷去定义自定义组件的外观和行为</p>
                        </div>
                    ),
                    foot: <div>尾部</div>
                }}
            </Dialog>
        </div>
    );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

两个{{ :第一个{是表达式,第二个{是对象。

# 实现作用域插槽

import React from "react";

function Dialog(props) {
    // 备选消息
    const messages = {
        "foo": { title: 'foo', content: 'foo~' },
        "bar": { title: 'bar', content: 'bar~' },
    }
    // 执行函数获得要显示的内容
    const { body, footer } = props.children(messages[props.msg]);
    return (
        <div style={{ border: "1px solid blue" }}>
            {/* 此处显示的内容是动态生成的 */}
            {body}
            <div>{footer}</div>
        </div>
    );
}
export default function Composition() {
    return (
        <div>
            {/* 执行显示消息的key */}
            <Dialog msg="foo">
                {/* 修改为函数形式,根据传入值生成最终内容 */}
                {({ title, content }) => ({
                    body: (
                        <div>
                            <h1>{title}</h1>
                            <p>{content}</p>
                        </div>
                    ),
                    footer: <button onClick={() => alert("react确实好")}>确定</button>
                })}
            </Dialog>
        </div>
    );
}    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

如果props.children是jsx,此时它是不能修改的,如果我们要修改chidren,则可以通过克隆的方式实现。

范例:实现如下RadioGroup和Radio组件,我们希望只在RadioGroup里面写name属性,然后通过react给每个子组件Radio都添加上name属性,那么可以这么做。

<RadioGroup name="mvvm">
    <Radio value="vue">vue</Radio>
    <Radio value="react">react</Radio>
    <Radio value="ng">angular</Radio>
</RadioGroup>
1
2
3
4
5

实现方法:

function RadioGroup(props) {
    return (
        // 遍历props.children
        <div>
            {React.Children.map(props.children, child => {
                // 要修改child属性必须先克隆它
                // 参数1是克隆对象
                // 参数2是设置的属性
                return React.cloneElement(child, { name: props.name });
            })}
        </div>
    );
}
// Radio传入value,name和children,注意区分
function Radio({ children, ...rest }) {
    // props解构成了children和rest,children用来渲染文本内容vue,react和angular。rest用来渲染radio的其它属性。
    return (
        <label>
            <input type="radio" {...rest} />
            {children}
        </label>
    );
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

React.Children是react提供的静态属性,map方法第一个参数是需要遍历的对象,第二个参数是遍历时的逻辑函数。

React.cloneElement是react提供的静态方法,用来克隆dom,第一个参数是要克隆的节点。

# 四、组件设计与实现

# 表单组件

import React, { Component } from "react";
import { Input, Button } from "antd";

// 创建高阶组件
function kFormCreate(Comp) {
  return class extends Component {
    constructor(props) {
      super(props);

      this.options = {}; //表单配置项
      this.state = {
        //   usernameMessage: 'lalalala'
      }; // 表单值
    }

    // 全局校验
    validateFields = cb => {
      //   console.log(this.state);
      const rets = Object.keys(this.options).map(field => {
        return this.validateField(field);
      });
      const ret = rets.every(v => v);
      // 将校验结果传出去,并传递数据
      cb(ret, this.state);
    };

    // 单项校验
    validateField = field => {
      // 校验规则
      const { rules } = this.options[field];
      // 校验: ret如果是false校验失败
      const ret = !rules.some(rule => {
        if (rule.required) {
          // 获取校验项的值
          if (!this.state[field]) {
            // 必填项失败
            // 设置错误信息
            this.setState({
              [field + "Message"]: rule.message
            });
            return true;
          }
        }

        return false;
      });

      // 若校验成功,清理错误信息
      if (ret) {
        this.setState({
          [field + "Message"]: ""
        });
      }

      return ret;
    };

    // 变更处理
    handleChange = e => {
      const { name, value } = e.target;
      this.setState(
        {
          [name]: value
        },
        () => {
          this.validateField(name);
        }
      );
    };

    getFieldDec = (field, option) => {
      this.options[field] = option;

      // 返回一个装饰器(高阶组件)
      return InputComp => {
        return (
          <div>
            {React.cloneElement(InputComp, {
              name: field, // 控件name
              value: this.state[field] || "",
              onChange: this.handleChange // 输入值变化监听回调
            })}
            {/* 校验错误信息 */}
            {this.state[field + "Message"] && (
              <p style={{ color: "red" }}>{this.state[field + "Message"]}</p>
            )}
          </div>
        );
      };
    };

    render() {
      return (
        <Comp
          {...this.props}
          getFieldDec={this.getFieldDec}
          validateFields={this.validateFields}
        />
      );
    }
  };
}

@kFormCreate
class KFormTest extends Component {
  onLogin = () => {
    // 校验
    this.props.validateFields((isValid, data) => {
      if (isValid) {
        console.log("登录!!!!");
      } else {
        alert("校验失败");
      }
    });
  };

  render() {
    const { getFieldDec } = this.props;
    return (
      <div>
        {/* 接收两个参数返回一个装饰器 */}
        {getFieldDec("username", {
          rules: [{ required: true, message: "请输入用户名" }]
        })(<Input type="text" />)}

        {getFieldDec("password", {
          rules: [{ required: true, message: "请输入密码" }]
        })(<Input type="password" />)}
        <Button onClick={this.onLogin}>登录</Button>
      </div>
    );
  }
}

export default KFormTest;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135

# 弹窗类组件

弹窗类组件的要求弹窗内容在A处声明,却在B处展示。react v16之后出现的portal可以实现内容传送功能。

import React, { Component } from "react";
import {
  createPortal,
  unmountComponentAtNode,
  unstable_renderSubtreeIntoContainer
} from "react-dom";

export class Dialog2 extends React.Component {
  // render一个null,目的什么内容都不渲染
  render() {
    return null;
  }

  componentDidMount() {
    // 首次挂载时候创建宿主div
    const doc = window.document;
    this.node = doc.createElement("div");
    doc.body.appendChild(this.node);

    this.createPortal(this.props);
  }

  componentDidUpdate() {
    this.createPortal(this.props);
  }

  componentWillUnmount() {
    // 清理节点
    unmountComponentAtNode(this.node);
    //   清理宿主div
    window.document.body.removeChild(this.node);
  }

  createPortal(props) {
    unstable_renderSubtreeIntoContainer(
      this, //当前组件
      <div className="dialog">{props.children}</div>, // 塞进传送门的JSX
      this.node // 传送门另一端的DOM node
    );
  }
}

export default class Dialog extends Component {
  constructor(props) {
    super(props);

    this.node = document.createElement("div");
    document.body.appendChild(this.node);
  }

  render() {
    // 将createPortal参数1声明的jsx挂载到node上
    return createPortal(<div>{this.props.children}</div>, this.node);
  }

  // 清理div
  componentWillUnmount() {
    document.body.removeChild(this.node);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

# 树形组件

import React, { Component } from "react";

class TreeNode extends Component {
    constructor(props) {
        super(props);
        this.state = {
            isOpen: false
        }
    }
    get isFolder() {
        return this.props.model.children && this.props.model.children.length;
    }
    toggle = () => {
        this.setState({
            isOpen: !this.state.isOpen
        })
    }
    render() {
        return (
            <ul>
                <li>
                    <div onClick={this.toggle}>
                        {this.props.model.title}
                        {this.isFolder ? <span>{this.state.isOpen ? '-' : '+'}</span> : null}
                    </div>
                    {this.isFolder ?
                        <div style={{ display: this.state.isOpen ? 'block' : 'none' }}>
                            {this.props.model.children.map(model => {
                                return <TreeNode model={model} key={model.title} />
                            })}
                        </div>
                        : null}
                </li>
            </ul>
        )
    }
}

export default class Tree1 extends Component {
    treeData = {
        title: "编程语言",
        children: [
            {
                title: "Java"
            },
            {
                title: "JS",
                children: [
                    {
                        title: "ES6"
                    },
                    {
                        title: "ES5"
                    }
                ]
            },
            {
                title: "Web前端",
                children: [
                    {
                        title: "Vue",
                        expand: true,
                        children: [
                            {
                                title: "组件化"
                            },
                            {
                                title: "源码"
                            },
                            {
                                title: "docker部署"
                            }
                        ]
                    },
                    {
                        title: "React",
                        children: [
                            {
                                title: "JSX"
                            },
                            {
                                title: "虚拟DOM"
                            }
                        ]
                    },
                    {
                        title: "Node"
                    }
                ]
            }
        ]
    };
    render() {
        return (
            <div>
                <TreeNode model={this.treeData} />
            </div>
        );
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100

# 五、常见组件优化技术

  • 定制组件的shouldComponentUpdate钩子
  • PureComponent
  • React.memo

React v16.6.0 之后的版本,可以使用一个新功能 React.memo 来完美实现让函数式的组件也有了PureComponent的功能。

  • Immutable.js
Last Updated: 3/22/2020, 3:45:51 PM