静态组件和动态组件

静态组件

React中的函数组件是无状态的,当在函数组件内部定义一个变量,并尝试在事件处理函数中修改这个变量时,确实,组件内部的变量值会改变,但这个改变不会触发组件的重新渲染,因为它不是一个状态变量。React依赖状态(state)和属性(props)的改变来决定是否需要重新渲染组件。如果这些值不变,React就假定组件输出不需要更新。

举个例子,如果你这样做:

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
/*
* @Description:
* @Author: xiuji
* @Date: 2023-12-07 10:56:02
* @LastEditTime: 2023-12-08 13:55:52
* @LastEditors: Do not edit
*/
import React from "react";
import PropTypes from 'prop-types';

const ClassCom = ({sumA,sumB}) => {
let count = 0;
return (
<div>
<div>
<div>sumA: {sumA}</div>
<div>sumB: {sumB}</div>
<div>count: {count}</div>
</div>
<div>
<button onClick={() => {
sumA++;
console.log('sumA', sumA)
}}>sumA增加</button>
<button onClick={() => {
sumB++;
console.log('sumB', sumB)
}}>sumB增加</button>
<button onClick={() => {
count = sumA + sumB;
console.log('count', count)
}}>count = sumA + sumB</button>
</div>
</div>
)
}

ClassCom.propTypes = {
sumA: PropTypes.number.isRequired,
sumB: PropTypes.number.isRequired
}

export default ClassCom;

在上面的代码中,点击按钮会增加sumA、sumB、count的值,但是不会触发组件的重新渲染。因为sumA、sumB、count不是一个状态变量,所以React不会关注它的变化,因此变量虽然变化,但是视图不会更新。

动态组件

动态组件是指那些根据用户交互、数据请求或任何其他类型的异步事件来改变其行为和/或外观的组件。在React中,这通常意味着组件的状态(state)会随时间发生变化,从而导致UI的更新。动态组件能够响应用户输入,如点击、滚动、键盘输入等,或者根据从外部获取的数据来改变展示内容。

类组件并不默认就是动态组件,它们成为动态的是因为它们的状态(state)可以被改变。类组件通过内部状态管理(使用this.statethis.setState)来实现动态行为。当内部状态发生变化时,React会重新渲染组件,从而反映状态的最新值。

函数组件在引入Hooks之前,通常被视为无状态组件,只能接受props并渲染它们。但现在,函数组件可以使用useState这样的Hook来引入和管理内部状态,以及使用useEffect和其他Hooks来处理副作用和订阅模式。useState返回一个状态变量和一个更新这个状态变量的函数,使得函数组件能够像类组件一样处理动态行为。

动态组件是由于实现了状态管理和响应生命周期事件的能力。通过这些机制,它们能够根据外部和内部的变化来更新UI。

函数组件+Hooks组合我们后边另开篇幅详细讲解,下边先了解一下React中的类组件。

ES6Class类基础知识复习

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
class Parent {
// 通过static关键字添加静态私有属性/方法,把构造函数当做对象,添加属性
static name = 'parent';

constructor() {
// 通过this关键字添加实例属性
this.age = 18;
}
// 通过变量=属性的方法添加私有属性,等价于在constructor中使用this.age = 18
sex = 'man';

// 通过变量=箭头函数的方法添加私有方法
getAge = () => {
// 箭头函数没有this,所用到的this是宿主环境的this(这里是Parent类)
return this.age;
}

// getName() 类似于 getName = function() {},这种写法会将getName方法挂载到原型上,为公有方法,不可枚举
getName() {
return this.name;
}
}
// 为Parent类添加公共属性
Parent.prototype.school = 'xxx';
const p = new Parent();

console.log(p)查看类实例的内部属性和方法

使用类需要先实例化

1
2
const p = new Parent();
console.log(p.getAge); // 18

通过static关键字添加的静态属性和方法需要通过console.dir(Parent)查看

使用类的公共方法

1
console.log(Parent.getName) // parent
extends关键字继承
1
2
3
4
5
class Parent extends React.Component {

}
const p = new Parent();
console.log(p);

1、首先基于call继承

1
2
3
4
5
6
7
8
9
10
11
12
// 首先基于call继承,将React.Component当作函数执行,将this指向Parent实例
// React.Component参见React源码---node_modules/react/umd/react.development.js---303行
function Component(props, context, updater) {
this.props = props;
this.context = context; // If a component has string refs, we will assign a different object later.

this.refs = emptyObject; // We initialize the default updater but the real one gets injected by the
// renderer.

this.updater = updater || ReactNoopUpdateQueue;
}
为创建的实例p设置四个私有属性:props、context、refs、undater

2、再基于原型继承

继承层次如下:

实例 -> Parent.Prototype -> React.Component.Prototype -> Object.Prototype

实例除了具备Parent.Prototype提供的方法之外,还具备 React.Component.Prototype原型上提供的方法:forceUpdateisReactComponentsetState

React类组件

将上文中的无状态函数组件转换成类组件:

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
class ClassCom extends React.Component {
// 设置默认props
// static defaultProps = {
// sumA: 0,
// sumB: 0
// }

static propTypes = {
sumA: PropTypes.number.isRequired,
sumB: PropTypes.number.isRequired
}

constructor(props) {
super(props);
// 初始化状态
this.state = {
sumA: props.sumA,
sumB: props.sumB,
count: 0
}
}

// 组件将要挂载
// componentWillMount() { // 即将废弃,不建议使用,可使用UNSAFE_componentWillMount代替(去除控制台警告)
// console.log('组件将要挂载')
// }

UNSAFE_componentWillMount() { // 即将废弃,不建议使用
console.log('组件将要挂载')
}

render() { // 渲染函数
console.log('渲染');
let { sumA, sumB,count } = this.state;
return (
<div>
<div>
<div>sumA: {sumA}</div>
<div>sumB: {sumB}</div>
<div>count: {count}</div>
</div>
<div>
<button onClick={() => {
this.setState({sumA: sumA+1});
console.log('sumA', sumA)
}}>sumA增加</button>
<button onClick={() => {
this.setState({sumB: sumB+1}); // 修改状态并触发视图更新
console.log('sumB', sumB)
}}>sumB增加</button>
<button onClick={() => {
this.state.count = sumA + sumB;
this.forceUpdate(); // 强制更新视图
console.log('count', count)
}}>count = sumA + sumB</button>
</div>
</div>
)
}

// 组件挂载完成,已经将Virtual DOM挂载到真实DOM上
componentDidMount() {
console.log('组件挂载完成')
}

// 组件更新
shouldComponentUpdate(nextProps, nextState) {
console.log('nextProps', nextProps);
console.log('nextState', nextState);
// 返回true,组件更新;返回false,组件不更新
return true;
}

// 组件将要更新
UNSAFE_componentWillUpdate() { // 即将废弃,不建议使用
console.log('组件将要更新')
}

// 组件更新完成
componentDidUpdate() {
console.log('组件更新完成')
}
}

export default ClassCom;

类组件第一次渲染逻辑

类组件更新状态的渲染逻辑

PS:如果基于this.forceUpdate()方法强制更新视图,会跳过shouldComponentUpdate周期函数的校验,直接从UNSAFE_componentWillUpdate开始进行更新(这通常是不推荐的,因为它可能导致不可预测的渲染行为)

父组件更新导致子组件需要更新的逻辑

React在执行组件树的更新时遵循深度优先遍历的原则。当React开始渲染或更新过程时,它会从根组件开始,并按照深度优先的顺序逐个处理每个组件。这意味着,在更新父组件之前,React会先调用所有子组件的渲染方法,并在必要时进行更新。

需要注意的是,父组件的componentDidUpdate是在所有子组件的componentDidUpdate调用之后才会被调用的。这保证了子组件的props来自于其父组件最新的状态,并反映了React的自顶向下数据流的设计原则。

PureComponent和Component的区别

React.PureComponentReact.Component 都是 React 应用中创建类组件的方式,但它们在组件的更新行为上有一些关键的区别。

React.Component

React.Component 是创建类组件的基础。如果一个组件继承自 React.Component,那么它需要手动实现 shouldComponentUpdate 方法来确定组件是否需要更新。默认情况下,当组件的 props 或 state 发生变化时,组件会重新渲染。

React.PureComponent

React.PureComponentReact.Component 相似,但它通过对 props 和 state 进行浅比较来实现了一个具有浅层比较能力的 shouldComponentUpdate 方法。如果组件的 props 和 state 之前和现在都相等(浅比较),那么组件不会重新渲染。

这意味着使用 PureComponent 可以提高性能,因为它减少了不必要的渲染。但是,使用时需要确保所有的 props 和 state 都是不可变的,因为 PureComponent 的浅比较可能会错过深层数据结构的变化,从而导致组件不更新。

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
class MyPureComponent extends React.PureComponent {

state = {
arr: [10,20,30]
}

render() {
let { arr } = this.state;
return (
<div>
{
arr.map((item, index) => {
return <div key={index}>{item}</div>
})
}
<button onClick={() => {
arr.push(40);
console.log('====================================');
console.log(arr);
console.log('====================================');
this.setState({arr});
}}>add div</button>
</div>
)
}
}

点击按钮后:

可见组件state状态已改变,但页面并未更新,只因PureComponentshouldComponentUpdate中对state进行浅比较未发现变动的地方。

向数组arr中push数据,将arr的值改变了,引用地址未变,浅比较无法比较出区别
1
2
3
4
5
6
7
<button onClick={() => {
arr.push(40);
console.log('====================================');
console.log(arr);
console.log('====================================');
this.setState({arr:[...arr]}); // 将值更新后的数组赋值给新的数组,引用地址变化,页面更新
}}>add div</button>

主要区别

  • 更新机制React.Component 默认情况下不实现 shouldComponentUpdate,而 React.PureComponent 实现了一个具有浅比较逻辑的 shouldComponentUpdate
  • 性能优化React.PureComponent 通过避免不必要的更新来优化性能,但前提是组件的 props 和 state 都是不可变的。
  • 使用场景:如果你知道一个组件的 props 和 state 结构简单,不会发生深层数据变化,那么使用 PureComponent 可以很容易地获得性能提升。相反,如果有复杂的数据结构或需要深层比较,可能就需要手动优化 shouldComponentUpdate,或者使用 React.Component

总的来说,PureComponent 可以减少组件的渲染次数从而优化性能,但是在使用时需要小心数据结构的变化。如果组件经常依赖深层数据变动或者数据变动非常复杂,那么可能需要使用 React.Component 并手动实现 shouldComponentUpdate 以获得更精确的控制。

手动实现对象浅比较

Component中手动实现shouldComponentUpdate内对象浅比较以达到PureComponent的效果

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
// 是否为对象
const isObject = (obj) => {
return obj !== null && /^(object|function)$/.test(typeof obj);
}
// 对象浅比较
const shallowEqual = (obj1, obj2) => {
// 判断是否为对象
if (!isObject(obj1) || !isObject(obj2)) return false;
// 直接比较内存地址
if (obj1 === obj2) return true;
// 获取对象的属性
let keys1 = Object.keys(obj1),
keys2 = Object.keys(obj2);
// 比较属性数量
if (keys1.length !== keys2.length) return false;
// 数量一致,遍历比较对象第一级属性值
for (let i = 0; i< keys1.length; i++) {
let key = keys1[i];
// 对象a中有属性a,但是对象b中没有属性a,或者属性a的值不相等,返回false
// if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) return false;
// obj1[key] === obj2[key]无法比较NaN,使用Object.is()比较
if (!obj2.hasOwnProperty(key) || !Object.is(obj1[key], obj2[key])) return false;
}
return true;
}

class MyComponent extends React.Component {
state = {
arr: [10,20,30]
}

render() {
let { arr } = this.state;
return (
<div>
{
arr.map((item, index) => {
return <div key={index}>{item}</div>
})
}
<button onClick={() => {
arr.push(40);
console.log('====================================');
console.log(arr);
console.log('====================================');
this.setState({arr});
}}>add div</button>
</div>
)
}

// 组件更新
shouldComponentUpdate(nextProps, nextState) {
// nextProps是新的props,nextState是新的state
// this.props是旧的props,this.state是旧的state
let { props, state } = this;
// 浅比较新旧props和state,如果有不同则返回true,否则返回false
return !shallowEqual(props, nextProps) || !shallowEqual(state, nextState);
}
}

测试效果与PureComponent一致。