# 进阶
# 属性默认值 和 类型检查
# 属性默认值
通过一个静态属性defaultProps
告知 react 属性默认值
- 函数组件
import React from "react";
export default function FuncDefault(props) {
console.log(props); //已经完成了混合
return (
<div>
a:{props.a},b:{props.b},c:{props.c}
</div>
);
}
// 属性默认值
FuncDefault.defaultProps = {
a: 1,
b: 2,
c: 3,
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 类组件
import React from "react";
export default class ClassDefault extends React.Component {
static defaultProps = {
a: 1,
b: 2,
c: 3,
};
constructor(props) {
super(props);
console.log(props);
}
render() {
return (
<div>
a:{this.props.a},b:{this.props.b},c:{this.props.c}
</div>
);
}
}
// // 属性默认值
// ClassDefault.defaultProps = {
// a: 1,
// b: 2,
// c: 3
// }
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
# 属性类型检查
使用库:prop-types
对组件使用静态属性propTypes
告知 react 如何检查属性
PropTypes.any://任意类型
PropTypes.array://数组类型
PropTypes.bool://布尔类型
PropTypes.func://函数类型
PropTypes.number://数字类型
PropTypes.object://对象类型
PropTypes.string://字符串类型
PropTypes.symbol://符号类型
PropTypes.node://任何可以被渲染的内容,字符串、数字、React元素
PropTypes.element://react元素
PropTypes.elementType://react元素类型
PropTypes.instanceOf(构造函数)://必须是指定构造函数的实例
PropTypes.oneOf([xxx, xxx])://枚举
PropTypes.oneOfType([xxx, xxx]); //属性类型必须是数组中的其中一个
PropTypes.arrayOf(PropTypes.XXX)://必须是某一类型组成的数组
PropTypes.objectOf(PropTypes.XXX)://对象由某一类型的值组成
PropTypes.shape(对象): //属性必须是对象,并且满足指定的对象要求
PropTypes.exact({...})://对象必须精确匹配传递的数据
//自定义属性检查,如果有错误,返回错误对象即可
属性: function(props, propName, componentName) {
//...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { Component } from "react";
import PropTypes from "prop-types";
export class A {}
export class B extends A {}
export default class ValidationComp extends Component {
//先混合属性
static defaultProps = {
b: false,
};
//再调用相应的函数进行验证
static propTypes = {
a: PropTypes.number.isRequired, //a属性必须是一个数字类型,并且必填
b: PropTypes.bool.isRequired, //b必须是一个bool属性,并且必填
onClick: PropTypes.func, //onClick必须是一个函数
c: PropTypes.any, //1. 可以设置必填 2. 阵型保持整齐(所有属性都在该对象中)
d: PropTypes.node.isRequired, //d必填,而且必须是一个可以渲染的内容,字符串、数字、React元素
e: PropTypes.element, //e必须是一个React元素
F: PropTypes.elementType, //F必须是一个组件类型
g: PropTypes.instanceOf(A), //g必须是A的实例
sex: PropTypes.oneOf(["男", "女"]), //属性必须是数组当中的一个
h: PropTypes.arrayOf(PropTypes.number), //数组的每一项必须满足类型要求
i: PropTypes.objectOf(PropTypes.number), //每一个属性必须满足类型要求
j: PropTypes.shape({
//属性必须满足该对象的要求
name: PropTypes.string.isRequired, //name必须是一个字符串,必填
age: PropTypes.number, //age必须是一个数字
address: PropTypes.shape({
province: PropTypes.string,
city: PropTypes.string,
}),
}),
k: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
})
),
m: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
score: function (props, propName, componentName) {
console.log(props, propName, componentName);
const val = props[propName];
//必填
if (val === undefined || val === null) {
return new Error(`invalid prop ${propName} in ${componentName},${propName} is Required`);
}
//该属性必须是一个数字
if (typeof val !== "number") {
return new Error(`invalid prop ${propName} in ${componentName},${propName} is not a number`);
}
const err = PropTypes.number.isRequired(props, propName, componentName);
if (err) {
return err;
}
//并且取值范围是0~100
if (val < 0 || val > 100) {
return new Error(`invalid prop ${propName} in ${componentName},${propName} must is between 0 and 100`);
}
},
};
render() {
const F = this.props.F;
return (
<div>
{this.props.a}
<div>
{this.props.d}
<F />
</div>
</div>
);
}
}
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
# HOC 高阶组件
HOF:Higher-Order Function, 高阶函数,以函数作为参数,并返回一个函数 HOC: Higher-Order Component, 高阶组件,以组件作为参数,并返回一个组件
通常,可以利用 HOC 实现横切关注点。
举例:20 个组件,每个组件在创建组件和销毁组件时,需要作日志记录 20 个组件,它们需要显示一些内容,得到的数据结构完全一致
注意
- 不要在 render 中使用高阶组件
- 不要在高阶组件内部更改传入的组件
import React from "react";
/**
* 高阶组件,命名通常以with开头
* @param {*} comp 组件
*/
export default function withLog(Comp, str) {
return class LogWrapper extends React.Component {
componentDidMount() {
console.log(`日志:组件${Comp.name}被创建了!${Date.now()}`);
}
componentWillUnmount() {
console.log(`日志:组件${Comp.name}被销毁了!${Date.now()}`);
}
render() {
return (
<>
<h1>{str}</h1>
<Comp {...this.props} />
</>
);
}
};
}
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
# ref
reference: 引用
场景:希望直接使用 dom 元素中的某个方法,或者希望直接使用自定义组件中的某个方法
- ref 作用于内置的 html 组件,得到的将是真实的 dom 对象
- ref 作用于类组件,得到的将是类的实例
- ref 不能作用于函数组件
import React, { Component } from "react";
class A extends Component {
method() {
console.log("调用了组件A的方法");
}
render() {
return <h1>组件A</h1>;
}
}
// function B(){
// return <h1>组件B</h1>
// }
export default class Comp extends Component {
handleClick = () => {
console.log(this);
this.refs.txt.focus();
this.refs.compA.method();
};
render() {
return (
<div>
<input ref="txt" type="text" />
<A ref="compA" />
{/* <B ref="compB" /> */}
<button onClick={this.handleClick}>聚焦</button>
</div>
);
}
}
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
ref 不再推荐使用字符串赋值,字符串赋值的方式将来可能会被移出
目前,ref 推荐使用对象或者是函数
# 对象
通过 React.createRef 函数创建
import React, { Component } from "react";
export default class Comp extends Component {
constructor(props) {
super(props);
this.txt = React.createRef();
}
handleClick = () => {
this.txt.current.focus();
};
render() {
return (
<div>
<input ref={this.txt} type="text" />
<button onClick={this.handleClick}>聚焦</button>
</div>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 函数
import React, { Component } from "react";
export default class Comp extends Component {
state = {
show: true,
};
handleClick = () => {
// this.txt.focus();
this.setState({
show: !this.state.show,
});
};
componentDidMount() {
console.log("didMount", this.txt);
}
getRef = (el) => {
console.log("函数被调用了", el);
this.txt = el;
};
render() {
return (
<div>
{this.state.show && <input ref={this.getRef} type="text" />}
<button onClick={this.handleClick}>显示/隐藏</button>
</div>
);
}
}
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
函数的调用时间:
- componentDidMount 的时候会调用该函数
- 在 componentDidMount 事件中可以使用 ref
- 如果 ref 的值发生了变动(旧的函数被新的函数替代),分别调用旧的函数以及新的函数,时间点出现在 componentDidUpdate 之前
- 旧的函数被调用时,传递 null
- 新的函数被调用时,传递对象
- 如果 ref 所在的组件被卸载,会调用函数
谨慎使用 ref
能够使用属性和状态进行控制,就不要使用 ref。
- 调用真实的 DOM 对象中的方法
- 某个时候需要调用类组件的方法
# Ref 转发
forwardRef
forwardRef 方法:
- 参数,传递的是函数组件,不能是类组件,并且,函数组件需要有第二个参数来得到 ref
- 返回值,返回一个新的组件
import React from "react";
function A(props, ref) {
return (
<h1 ref={ref}>
组件A
<span>{props.words}</span>
</h1>
);
}
//传递函数组件A,得到一个新组件NewA
const NewA = React.forwardRef(A);
export default class App extends React.Component {
ARef = React.createRef();
componentDidMount() {
console.log(this.ARef);
}
render() {
return (
<div>
<NewA ref={this.ARef} words="asfsafasfasfs" />
{/* this.ARef.current: h1 */}
</div>
);
}
}
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
import React from "react";
class A extends React.Component {
render() {
return (
<h1 ref={this.props.abc}>
组件A
<span>{this.props.words}</span>
</h1>
);
}
}
const NewA = React.forwardRef((props, ref) => {
return <A {...props} abc={ref} />;
});
export default class App extends React.Component {
ARef = React.createRef();
componentDidMount() {
console.log(this.ARef);
}
render() {
return (
<div>
<NewA ref={this.ARef} words="asfsafasfasfs" />
{/* this.ARef.current: h1 */}
</div>
);
}
}
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
# Context
上下文:Context,表示做某一些事情的环境
React 中的上下文特点:
- 当某个组件创建了上下文后,上下文中的数据,会被所有后代组件共享
- 如果某个组件依赖了上下文,会导致该组件不再纯粹(外部数据仅来源于属性 props)
- 一般情况下,用于第三方组件(通用组件)
# 旧的 API
创建上下文
只有类组件才可以创建上下文
- 给类组件书写静态属性 childContextTypes,使用该属性对上下文中的数据类型进行约束
- 添加实例方法 getChildContext,该方法返回的对象,即为上下文中的数据,该数据必须满足类型约束,该方法会在每次 render 之后运行。
使用上下文中的数据
要求:如果要使用上下文中的数据,组件必须有一个静态属性 contextTypes,该属性描述了需要获取的上下文中的数据类型
- 可以在组件的构造函数中,通过第二个参数,获取上下文数据
- 从组件的 context 属性中获取
- 在函数组件中,通过第二个参数,获取上下文数据
上下文的数据变化
上下文中的数据不可以直接变化,最终都是通过状态改变
在上下文中加入一个处理函数,可以用于后代组件更改上下文的数据
# 新版 API
旧版 API 存在严重的效率问题,并且容易导致滥用
创建上下文
上下文是一个独立于组件的对象,该对象通过 React.createContext(默认值)创建
返回的是一个包含两个属性的对象
- Provider 属性:生产者。一个组件,该组件会创建一个上下文,该组件有一个 value 属性,通过该属性,可以为其数据赋值
- 同一个 Provider,不要用到多个组件中,如果需要在其他组件中使用该数据,应该考虑将数据提升到更高的层次
- Consumer 属性:后续讲解
使用上下文中的数据
- 在类组件中,直接使用 this.context 获取上下文数据
- 要求:必须拥有静态属性 contextType , 应赋值为创建的上下文对象
- 在函数组件中,需要使用 Consumer 来获取上下文数据
- Consumer 是一个组件
- 它的子节点,是一个函数(它的 props.children 需要传递一个函数)
注意细节
如果,上下文提供者(Context.Provider)中的 value 属性发生变化(Object.is 比较),会导致该上下文提供的所有后代元素全部重新渲染,无论该子元素是否有优化(无论 shouldComponentUpdate 函数返回什么结果)
import React, { Component } from "react";
const ctx = React.createContext();
function ChildA(props) {
return (
<div>
<h1>ChildA</h1>
<h2>
<ctx.Consumer>
{(value) => (
<>
{value.a},{value.b}
</>
)}
</ctx.Consumer>
</h2>
<ChildB />
</div>
);
}
class ChildB extends React.Component {
static contextType = ctx;
render() {
return (
<p>
ChildB,来自于上下文的数据:a: {this.context.a}, b:{this.context.b}
<button
onClick={() => {
this.context.changeA(this.context.a + 2);
}}
>
后代组件的按钮,点击a+2
</button>
</p>
);
}
}
export default class NewContext extends Component {
state = {
a: 0,
b: "abc",
changeA: (newA) => {
this.setState({
a: newA,
});
},
};
render() {
return (
<ctx.Provider value={this.state}>
<div>
<ChildA />
<button
onClick={() => {
this.setState({
a: this.state.a + 1,
});
}}
>
父组件的按钮,a加1
</button>
</div>
</ctx.Provider>
);
}
}
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
# PureComponent
纯组件,用于避免不必要的渲染(运行 render 函数),从而提升效率
优化:如果一个组件的属性和状态,都没有发生变化,重新渲染组件是没有必要的
PureComponent 是一个组件,如果某个组件继承自该组件,则该组件的 shouldComponentUpdate 会进行优化,对属性和状态进行浅比较,如果相等则不会重新渲染
注意
- PureComponent 进行浅比较
- 为了效率, 应该尽量使用 PureComponent
- 要求不要改动之前的状态,永远是创建新的状态来覆盖之前的状态(Immutable,不可变对象)
- 有一个第三方 JS 库,Immutable.js, 它专门用于制作不可变对象
- 函数组件,使用 React.memo 函数制作纯组件
# render props
有时候,某些组件的各种功能及其处理逻辑几乎完全相同,只是显示的界面不一样,建议下面的方式认选其一来解决重复代码的问题(横切关注点)
- render props
- 某个组件,需要某个属性
- 该属性是一个函数,函数的返回值用于渲染
- 函数的参数会传递为需要的数据
- 注意纯组件的属性(尽量避免每次传递的 render props 的地址不一致)
- 通常该属性的名字叫做 render
import React, { PureComponent } from "react";
import "./style.css";
/**
* 该组件用于监听鼠标的变化
*/
export default class MouseListener extends PureComponent {
state = {
x: 0,
y: 0,
};
divRef = React.createRef();
handleMouseMove = (e) => {
//更新x和y的值
const { left, top } = this.divRef.current.getBoundingClientRect();
const x = e.clientX - left;
const y = e.clientY - top;
this.setState({
x,
y,
});
};
render() {
return (
<div ref={this.divRef} className="point" onMouseMove={this.handleMouseMove}>
{this.props.render ? this.props.render(this.state) : "默认值"}
</div>
);
}
}
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
import MouseListener from "./MouseListener";
import React from "react";
const renderPoint = (mouse) => (
<>
横坐标:{mouse.x},纵坐标:{mouse.y}
</>
);
const renderDiv = (mouse) => (
<>
<div
style={{
width: 100,
height: 100,
background: "#008c8c",
position: "absolute",
left: mouse.x - 50,
top: mouse.y - 50,
}}
></div>
</>
);
export default function Test() {
return (
<div>
<MouseListener render={renderPoint} />
<MouseListener render={renderDiv} />
</div>
);
}
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
- HOC
import React, { PureComponent } from "react";
import "./style.css";
export default function withMouseListener(Comp) {
return class MouseListener extends PureComponent {
state = {
x: 0,
y: 0,
};
divRef = React.createRef();
handleMouseMove = (e) => {
//更新x和y的值
const { left, top } = this.divRef.current.getBoundingClientRect();
const x = e.clientX - left;
const y = e.clientY - top;
this.setState({
x,
y,
});
};
render() {
return (
<div ref={this.divRef} className="point" onMouseMove={this.handleMouseMove}>
<Comp {...this.props} x={this.state.x} y={this.state.y} />
</div>
);
}
};
}
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
import withMouseListener from "./withMouseListener";
import React from "react";
function Point(props) {
return (
<>
横坐标:{props.x},纵坐标:{props.y}
</>
);
}
function MoveDiv(props) {
return (
<div
style={{
width: 100,
height: 100,
background: "#008c8c",
position: "absolute",
left: props.x - 50,
top: props.y - 50,
}}
></div>
);
}
const MousePoint = withMouseListener(Point);
const MouseDiv = withMouseListener(MoveDiv);
export default function Test() {
return (
<div>
<MousePoint />
<MouseDiv />
</div>
);
}
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