React 开发者工具

推荐使用Chrome或Edge浏览器,安装React Developer Tools(Facebook出品)。

image-20220213160959897

安装完成后,访问使用React编写的页面时,图标会高亮(开发环境为红色有debug标识,生产环境为蓝色),同时F12开发者工具中会多出ComponentsProfiler两个选项卡。

React组件

函数式组件

效果

image-20220213181322649

简单组件:无状态state

代码

1
2
3
4
5
6
7
// 1.创建函数式组件
function Demo() { // 创建组件,函数名首字母须大写
console.log(this) // 此处的this是undefined,因为babel编译后开启了严格模式
return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
}
// 2.渲染组件到页面
ReactDOM.render(<Demo/>, document.getElementById('test'))

执行ReactDOM.render(<Demo/>, document.getElementById('test'))后发生了什么?

  1. React解析组件标签,找到了Demo组件

  2. 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中

类式组件

JavaScript类

构造
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
// 创建一个Person类
class Person {
// 构造器方法
constructor(name, age) {
// 构造器中的this:类的实例对象
this.name = name
this.age = age
}
// 一般方法
speak() {
// speak()方法放在类的原型对象上,供实例使用
// 通过Person实例调用speak时,speak中的this就是Person实例
// 谁调用它,它指向谁
console.log(`我叫${this.name},我的年龄是${this.age}`)
}
}

// 创建一个Person的实例对象
const p1 = new Person('tom', 20)
const p2 = new Person('jerry', 22)

console.log(p1)
console.log(p2)

p1.speak()
p2.speak()

image-20220213192510026

继承
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
// 创建一个Student类,继承于Person类
class Student extends Person {
// 无新增属性可不写构造器方法
constructor(name, age, grade) {
super(name, age)
this.grade = grade
this.school = 'xxxx'
}
// 重写从父类继承的方法
speak() {
console.log(`我叫${this.name},我的年龄是${this.age},我今年读${this.grade}`)
}
study() {
// study()方法放在类的原型对象上,供实例使用
// 通过Student实例调用study时,study中的this就是Student实例
console.log('我每天努力学习')
}
}

// 创建一个Student的实例对象
const s1 = new Student('小王', 15)
const s2 = new Student('小张', 16, '高一')

console.log(s1)
console.log(s2)

s2.speak()

image-20220213192709261

  1. 类中的构造器不是必须写的,要对实例进行一些初始化的操作(如添加指定属性)时才写;
  2. 如果A类继承了B类,且A类中写了构造器,那么A类构造器中的super是必须要调用的;
  3. 类中所定义的方法,都是放在了类的原型对象上,供实例去使用

类中可以直接写赋值语句。

类中this的指向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
speak() {
console.log(this)
}
}

const p1 = new Person('tom', 18)
p1.speak()
const x = p1.speak
x()

image-20220218142424764

效果

屏幕截图 2022-02-13 194815

复杂组件:有状态state

代码

1
2
3
4
5
6
7
8
9
10
11
// 1.创建类式组件
class Demo extends React.Component {
render() {
// render方法放在Demo的原型对象上,供实例使用
// render中的this:Demo的实例对象
console.log('render中的this: ', this)
return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
}
}
// 2.渲染组件到页面
ReactDOM.render(<Demo/>, document.getElementById('test'))

执行ReactDOM.render(<Demo/>, document.getElementById('test'))后发生了什么?

  1. React解析组件标签,找到了Demo组件
  2. 发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法
  3. 将render返回的虚拟DOM转为真实DOM,随后呈现在页面中

简单组件与复杂组件的区别

简单组件:无状态 state

复杂组件:有状态 state

组件实例的三大核心属性

1.state

原生js中的事件绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<button id="btn1">按钮1</button>
<button id="btn2">按钮2</button>
<button onclick="demo()">按钮3</button>

<script>
const btn1 = document.getElementById('btn1')
btn1.addEventListener('click', ()=>{
alert('点击按钮1')
}) // 兼容性最好

const btn2 = document.getElementById('btn2')
btn2.onclick = ()=>{
alert('点击按钮2')
}

function demo() {
alert('点击按钮3')
}
</script>
</body>

React中的事件绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Weather extends React.Component {
constructor(props) {
super(props)
// 初始化状态
this.state = {isHot: true}
}
render() {
console.log(this)
// 读取状态
const {isHot} = this.state
return <h1 onClick={demo}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
}

function demo() {
console.log('标题被点击')
}

注意return <h1 onClick={demo}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>中html用法要改为驼峰,demo函数不需要加括号

类内的方法作为回调时,不通过实例调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Weather extends React.Component {
constructor(props) {
super(props)
// 初始化状态
this.state = {isHot: true}
}
render() {
// console.log(this)
// 读取状态
const {isHot} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
changeWeather() {
// changeWeather放在Weather的原型对象上,供实例使用
// 由于changeWeather是作为onClick的回调,所以不是通过实例调用的,是直接调用
// 类中的方法默认开启了局部的严格模式,所以changeWeather中的this是undefined
console.log(this)
}
}

image-20220218143656468

解决方法:在构造器中绑定

1
this.changeWeather = this.changeWeather.bind(this)

image-20220218143845258

image-20220218144055873

JavaScript中的bind:

1
2
3
4
5
6
function demo() {
console.log(this)
}
demo()
const x = demo.bind({a:1, b:'b'})
x()

image-20220218144700014

效果

GIF 2022-2-19 12-55-13

代码

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>三大属性 - state</title>
</head>
<body>
<div id="test"></div>

<!-- 引入 React 核心库 -->
<script type="text/javascript" src="../../js/react.development.js"></script>

<!-- 引入 react-dom -->
<script type="text/javascript" src="../../js/react-dom.development.js"></script>

<!-- 引入babel 将jsx转为js -->
<script type="text/javascript" src="../../js/babel.min.js"></script>

<script type="text/babel"> /* 此处必须为babel */

// 1.创建组件
class Weather extends React.Component {
// 构造器调用——1次
constructor(props) {
super(props)
// 初始化状态
this.state = {isHot: true}
// 绑定changeWeather中的this指向
this.changeWeather = this.changeWeather.bind(this)
}
// render调用——1+n次(初始化+状态更新次数)
render() {
// console.log(this)
// 读取状态
const {isHot} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
// changeWeather调用次数与点击次数相同
changeWeather() {
// changeWeather放在Weather的原型对象上,供实例使用
// 由于changeWeather是作为onClick的回调,所以不是通过实例调用的,是直接调用
// 类中的方法默认开启了局部的严格模式,所以changeWeather中的this是undefined

// 获取原来的isHot值
const isHot = this.state.isHot
// !!! 状态(state)不可直接更改,要借助一个内置的API去更改
// 状态必须通过setState进行更新,且更新是一种合并,不是替换,即:未涉及更新的参数不会丢失
this.setState({isHot: !isHot})

// 不可取: this.state.isHot = !isHot
}
}


// 2.渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))


</script>

</body>
</html>

state的简写方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Weather extends React.Component {

// 初始化状态
state = {isHot: true}

render() {
const {isHot} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}

// 自定义方法——赋值语句+箭头函数
changeWeather = ()=>{
const isHot = this.state.isHot
this.setState({isHot: !isHot})
}
}

理解

  1. state是组件对象最重要的属性,值是对象(可以包含多个key-value的组合)
  2. 组件被称为“状态机”,通过更新组件的state来更新对应的页面显示(重新渲染组件)

2.props

props是只读的

1
2
3
4
5
6
7
8
class Test extends React.Component {
render() {
console.log(this)
return <div></div>
}
}

ReactDOM.render(<Test key1="value1" key2="value2" />, document.getElementById('test'))

image-20220301203341626

批量传递props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person extends React.Component {

render() {
console.log(this)
const {name, age, sex} = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>年龄:{age}</li>
<li>性别:{sex}</li>
</ul>
)
}

}

const p = {name: 'Tom', age: 18, sex: '男'}
ReactDOM.render(<Person {...p} />, document.getElementById('test1'))
ReactDOM.render(<Person name='Amy' age={} sex='女' />, document.getElementById('test2'))

image-20220301204242744

展开运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let arr1 = [1, 3, 5, 7, 9]
let arr2 = [2, 4, 6, 8, 10]
console.log(arr1)
console.log(...arr1) // 展开一个数组

let arr3 = [...arr1, ...arr2] // 连接两个数组
console.log(arr3)

function sum(...numbers) { // 数组传参
return numbers.reduce((preValue, currentValue) => {
return preValue + currentValue
})
}
console.log(sum(1, 2, 3, 4, 5))

let person1 = { name: 'tom', age: 18 }
let person2 = { ...person1 } // 构造字面量对象
person1.name = 'jerry'
console.log('person2 :>> ', person2);

let person3 = { ...person1, name: 'jack', address: 'where' } // 复制对象并修改
console.log('person3 :>> ', person3);

image-20220301212807638

对props进行限制

原因:如果不对props加以限制,其他输入数据的人无法得知props需要的数据类型,例如ReactDOM.render(<Person name='Amy' age='19' sex='女' />, document.getElementById('test2'))ReactDOM.render(<Person name='Amy' age={19} sex='女' />, document.getElementById('test2'))

预期效果:

  • 姓名必须指定,且为字符串类型;
  • 性别为字符串类型,如果性别没有指定,默认为男
  • 年龄为字符串类型,且为数字类型,默认值为18

限制方法:

  1. 引入依赖包

    1
    <script type="text/javascript" src="../../js/prop-types.js"></script>
  2. 限制标签属性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Person.propTypes = {
    name: PropTypes.string.isRequired, // 限制name必传,为字符串
    sex: PropTypes.string, // 限制sex为字符串
    age: PropTypes.number, // 限制age为数值
    speak: PropTypes.func // 限制speak为函数
    }
    Person.defaultProps = {
    sex: '男',
    age: 18
    }
  3. 效果:

    1
    ReactDOM.render(<Person name={120}  />, document.getElementById('test2'))

    image-20220312104011074

props的简写方式

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
class Person extends React.Component {

// 进行限制
static propTypes = {
name: PropTypes.string.isRequired, // 限制name必传,为字符串
sex: PropTypes.string, // 限制sex为字符串
age: PropTypes.number, // 限制age为数值
speak: PropTypes.func // 限制speak为函数
}
static defaultProps = {
sex: '男',
age: 18
}

render() {
/* console.log(this) */
const {name, age, sex} = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>年龄:{age+1}</li>
<li>性别:{sex}</li>
</ul>
)
}

}

构造器与props

1
2
3
4
constructor(props) {
super(props)
console.log('constructor',this.props);
}

构造器是否接收props,是否传递给super,取决于是否希望在构造器中通过this访问props,极其罕见

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。

通常,在 React 中,构造函数仅用于以下两种情况:

函数式组件使用props

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(props) {
console.log(props);
const {name, age, sex} = props
return (
<ul>
<li>姓名:{name}</li>
<li>年龄:{age}</li>
<li>性别:{sex}</li>
</ul>
)
}

ReactDOM.render(<Person name='Jack' age={20} sex='男' />, document.getElementById('test3'))

image-20220312135358831

添加限制:

1
2
3
4
5
6
7
8
9
10
11
Person.propTypes = {
name: PropTypes.string.isRequired, // 限制name必传,为字符串
sex: PropTypes.string, // 限制sex为字符串
age: PropTypes.number, // 限制age为数值
}
Person.defaultProps = {
sex: '男',
age: 18
}

ReactDOM.render(<Person name={100}/>, document.getElementById('test3'))

image-20220312135740515

理解

  • 每个组件对象都会有props(properties的简写)属性
  • 组件标签的所有属性都保存在props中

作用:

  1. 通过标签属性从组件外向组件内传递变化的数据
  2. 注意: 组件内部不要修改props数据

3.refs

refs收集多组ref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Demo extends React.Component {

showThis = () => {
console.log(this)
}

render() {
return (
<div>
<input ref="input1" type="text" placeholder="点击按钮提示数据"/>&nbsp;
<button ref="button" onClick={this.showThis}>点击提示左侧输入框数据</button>&nbsp;
<input ref="input2" type="text" placeholder="失去焦点提示数据" />
</div>
)
}
}

image-20220312154333736

效果

GIF 2022-3-12 15-53-43

字符串形式的ref - 不太推荐

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
// 创建组件
class Demo extends React.Component {

// 展示左侧输入框数据
showLeft = () => {
const {input1} = this.refs
alert(input1.value)
}

// 展示右侧输入框数据
showRight = () => {
const {input2} = this.refs
alert(input2.value)
}

render() {
return (
<div>
<input ref="input1" type="text" placeholder="点击按钮提示数据"/>&nbsp;
<button onClick={this.showLeft}>点击提示左侧输入框数据</button>&nbsp;
<input ref="input2" onBlur={this.showRight} type="text" placeholder="失去焦点提示数据" />
</div>
)
}
}

// 渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test'))

过时 API:String 类型的 Refs

如果你之前使用过 React,你可能了解过之前的 API 中的 string 类型的 ref 属性,例如 "textInput"。你可以通过 this.refs.textInput 来访问 DOM 节点。我们不建议使用它,因为 string 类型的 refs 存在 一些问题。它已过时并可能会在未来的版本被移除。

回调函数形式的ref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Demo extends React.Component {

// 展示左侧输入框数据
showLeft = () => {
const {input1} = this
alert(input1.value)
}

// 展示右侧输入框数据
showRight = () => {
const {input2} = this
alert(input2.value)
}

render() {
return (
<div>
<input ref={(c) => {this.input1 = c}} type="text" placeholder="点击按钮提示数据"/>&nbsp;
<button onClick={this.showLeft}>点击提示左侧输入框数据</button>&nbsp;
<input ref={c => this.input2 = c} onBlur={this.showRight} type="text" placeholder="失去焦点提示数据" />
</div>
)
}
}

回调ref中的调用次数:

  • 首次渲染:调用1次
  • 页面更新:调用2次

关于回调 refs 的说明

如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。

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 Demo extends React.Component {

state = {isHot: true}

showInfo = () => {
const {input1} = this
alert(input1.value)
}

changeWeather = () => {
const {isHot} = this.state
this.setState({isHot: !isHot})
}

render() {
const {isHot} = this.state
return (
<div>
<h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2>
<input ref={(c) => {this.input1 = c; console.log('@', c)}} type="text" placeholder="点击按钮提示数据"/>&nbsp;
<button onClick={this.showInfo}>点击提示左侧输入框数据</button>&nbsp;
<button onClick={this.changeWeather}>点击修改天气</button>
</div>
)
}
}

GIF 2022-3-12 16-42-54

避免多次回调:将 ref 的回调函数定义成 class 的绑定函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
saveInput = (c) => {
this.input1 = c;
console.log('@', c)
}

render() {
const {isHot} = this.state
return (
<div>
<h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2>
<input ref={this.saveInput} type="text" placeholder="点击按钮提示数据"/>&nbsp;
<button onClick={this.showInfo}>点击提示左侧输入框数据</button>&nbsp;
<button onClick={this.changeWeather}>点击修改天气</button>
</div>
)
}

createRef API

官方最推荐的创建ref的形式

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
class Demo extends React.Component {

/*
React.createRef调用后可以返回一个容器,该容器可以存储被ref标识的节点,该容器是“专人专用”的
*/
myRefLeft = React.createRef()
myRefRight = React.createRef()

// 展示左侧输入框数据
showLeft = () => {
alert(this.myRefLeft.current.value)
}

// 展示右侧输入框数据
showRight = () => {
alert(this.myRefRight.current.value)
}

render() {
return (
<div>
<input ref={this.myRefLeft} type="text" placeholder="点击按钮提示数据"/>&nbsp;
<button onClick={this.showLeft}>点击提示左侧输入框数据</button>&nbsp;
<input ref={this.myRefRight} onBlur={this.showRight} type="text" placeholder="失去焦点提示数据" />
</div>
)
}
}

事件处理

  1. 通过onXxx属性指定事件处理函数(注意大小写)

    (1)React使用的是自定义(合成)事件, 而不是使用的原生DOM事件——为了更好的兼容性

    (2)React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)——高效

  2. 通过event.target得到发生事件的DOM元素对象——不要过度使用ref

收集表单数据

效果:

GIF 2022-3-12 20-15-45

非受控组件

页面内输入类DOM(input、checkbox、radio等)都是现用现取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Login extends React.Component {
handleSubmit = (event) => {
event.preventDefault() // 阻止默认事件——组织表单提交
const {username, password} = this
alert(`你输入的用户名是${username.value},密码是${password.value}`)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:<input ref={c => this.username = c} type="text" name="username" /><br/>
密码:<input ref={c => this.password = c} type="password" name="password" /><br/>
<button>登录</button>
</form>
)
}
}

受控组件

随着输入直接维护到状态中,需要时直接从状态中取得(类比:vue的双向数据绑定)

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
class Login extends React.Component {

// 初始化状态
state = {username: '', password: ''}

// 保存用户名和密码到状态中
saveUsername = (event) => {
this.setState({username:event.target.value})
}
savePassword = (event) => {
this.setState({password:event.target.value})
}

// 表单提交的回调
handleSubmit = (event) => {
event.preventDefault() // 阻止默认事件——组织表单提交
const {username, password} = this.state
alert(`你输入的用户名是${username},密码是${password}`)
}

render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveUsername} type="text" name="username" /><br/>
密码:<input onChange={this.savePassword} type="password" name="password" /><br/>
<button>登录</button>
</form>
)
}
}

GIF 2022-3-12 20-57-33

高阶函数与函数柯里化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 保存表单数据到状态中
saveFormData = (dataType) => {
return ((event) => {
this.setState({[dataType]:event.target.value})
})
}

render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveFormData('username')} type="text" name="username" /><br/>
密码:<input onChange={this.saveFormData('password')} type="password" name="password" /><br/>
<button>登录</button>
</form>
)
}

js方括号

1
2
3
4
5
6
7
8
9
10
let a = 'name'

let obj = {}
let obj1 = {}

obj[a] = 'tom'
obj1.a = 'tommy'

console.log(obj)
console.log(obj1)

image-20220315171516405

高阶函数

如果一个函数符合下面两个规范中的任何一个,那该函数就是高阶函数

  1. A函数接收的参数是一个函数
  2. A函数调用的返回值是一个函数

常见的高阶函数:PromisesetTimeoutarr.map()

函数的柯里化

通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*function sum(a, b, c) {
return a+b+c
}
result = sum(1, 2, 3)*/

function sum(a) {
return (b) => {
return (c) => {
return a+b+c
}
}
}
result = sum(1)(2)(3)
console.log(result);

不用柯里化的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 不用柯里化
saveFormDataN = (dataType, event) => {
this.setState({[dataType]:event.target.value})
}

render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:<input onChange={(event)=>{this.saveFormDataN('username', event)}} type="text" name="username" /><br/>
密码:<input onChange={(event)=>{this.saveFormDataN('password', event)}} type="password" name="password" /><br/>
<button>登录</button>
</form>
)
}

组件的生命周期

理解生命周期

生命周期回调函数 <=> 生命周期钩子函数 <=> 生命周期函数 <=> 生命周期钩子

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
// 创建组件
// 生命周期回调函数 <=> 生命周期钩子函数 <=> 生命周期函数 <=> 生命周期钩子
class Demo extends React.Component {
state = { opacity: 1 }

death = () => {
// 卸载组件
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}

// 组件挂载完毕调用
componentDidMount() {
this.timer = setInterval(() => {
// 获取原状态
let { opacity } = this.state
// -0.1
opacity -= 0.1
if (opacity <= 0) opacity = 1
// 设置新状态
this.setState({ opacity })
}, 200)
}

// 组件将要卸载调用
componentWillUnmount() {
clearInterval(this.timer)
}

// 初始化渲染、状态更新调用
render() {
console.log('render');
return (
<div>
<h2 style={{ opacity: this.state.opacity }}>React学不会了可咋办捏</h2>
<button onClick={this.death}>不活了</button>
</div>
)
}
}

// 渲染组件到页面
ReactDOM.render(<Demo />, document.getElementById('test'))
  1. 组件从创建到死亡它会经历一些特定的阶段。
  2. React组件中包含一系列钩子函数(生命周期回调函数), 会在特定的时刻调用。
  3. 我们在定义组件时,会在特定的生命周期回调函数中,做特定的工作。

旧版生命周期

流程图

react生命周期(旧)

setState()与forceUpdate()流程

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
// 创建组件
class Count extends React.Component {

// 构造器
constructor(props) {
console.log('Count - constructor')
super(props)
// 初始化状态
this.state = {count: 0}
}

// +1按钮的回调
add = () => {
// 获取原状态
const {count} = this.state
// 更新状态
this.setState({count: count+1})
}

// 卸载组件按钮的回调
death = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}

// 强制更新按钮的回调
force = () => {
this.forceUpdate()
}

// 组件将要挂载的钩子
componentWillMount() {
console.log('Count - componentWillMount');
}

// 组件挂载完毕的钩子
componentDidMount() {
console.log('Count - componentDidMount');
}

// 组件将要卸载的钩子
componentWillUnmount() {
console.log('Count - componentWillUnmount');
}

// 控制组件更新的“阀门”
shouldComponentUpdate() {
console.log('Count - shouldComponentUpdate');
return true // 重写shouldComponentUpdate()方法,必须有返回值
}

// 组件将要更新的钩子
componentWillUpdate() {
console.log('Count - componentWillUpdate');
}

// 组件更新完毕的钩子
componentDidUpdate() {
console.log('Count - componentDidUpdate');
}

render() {
console.log('Count - render');
const {count} = this.state
return (
<div>
<h2>当前计数为{count}</h2>
<button onClick={this.add}>点我+1</button>
<button onClick={this.death}>卸载组件</button>
<button onClick={this.force}>不更改状态强制更新组件</button>
</div>
)
}
}

// 渲染组件到页面
ReactDOM.render(<Count />, document.getElementById('test'))

setState

forceUpdate

父组件render流程

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
// 创建组件
class A extends React.Component {

state = {carName: '奔驰'}

change = () => {
this.setState({carName: '特斯拉'})
}

render() {
return (
<div>
<h2>我是A组件</h2>
<button onClick={this.change}>换车</button>
<B carName={this.state.carName}/>
</div>
)
}
}

class B extends React.Component {

// 组件将要接收新的props的钩子
componentWillReceiveProps(props) { // 坑:第一次调用的不算
console.log('B - componentWillReceiveProps', props);
}

// 控制组件更新的“阀门”
shouldComponentUpdate() {
console.log('B - shouldComponentUpdate');
return true // 重写shouldComponentUpdate()方法,必须有返回值
}

// 组件将要更新的钩子
componentWillUpdate() {
console.log('B - componentWillUpdate');
}

// 组件更新完毕的钩子
componentDidUpdate() {
console.log('B - componentDidUpdate');
}

render() {
console.log('B - render');
return(
<div>
<h2>我是B组件,我的车是{this.props.carName}</h2>
</div>
)
}
}

// 渲染组件到页面
ReactDOM.render(<A />, document.getElementById('test'))

父组件render

总结旧版生命周期的三个阶段

1.初始化阶段: 由ReactDOM.render()触发—初次渲染

  • constructor()
  • componentWillMount()
  • render()
  • componentDidMount() ====> 常用
    一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息

2.更新阶段: 由组件内部this.setState()或父组件重新render()触发

  • shouldComponentUpdate()
  • componentWillUpdate()
  • render() ====> 必须
  • componentDidUpdate()

3.卸载组件: 由ReactDOM.unmountComponentAtNode()触发

  • componentWillUnmount() ====> 常用
    一般在这个钩子中做收尾的事,例如:关闭定时器、取消订阅

新版生命周期

更新js文件,升级为17.0.1版本

1
2
3
4
5
6
7
8
<!-- 引入 React 核心库 -->
<script type="text/javascript" src="../js/17.0.1/react.development.js"></script>

<!-- 引入 react-dom -->
<script type="text/javascript" src="../js/17.0.1/react-dom.development.js"></script>

<!-- 引入babel 将jsx转为js -->
<script type="text/javascript" src="../js/17.0.1/babel.min.js"></script>

流程图

react生命周期(新)

getDerivedStateFromProps()

getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。

此方法适用于罕见的用例,即 state 的值在任何时候都取决于 props。例如,实现 <Transition> 组件可能很方便,该组件会比较当前组件与下一组件,以决定针对哪些组件进行转场动画。

1
2
3
4
5
6
7
8
9
class Count extends React.Component {
...
static getDerivedStateFromProps(props, state) {
console.log('getDerivedStateFromProps', props, state);
return props
}
...
}
ReactDOM.render(<Count count={568}/>, document.getElementById('test'))

GIF 2022-3-19 18-47-30

getSnapshotBeforeUpdate()

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()

此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。

应返回 snapshot 的值(或 null)。

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
class NewsList extends React.Component {

state = {newsArr: []}

componentDidMount() {
setInterval(() => {
const {newsArr} = this.state
const news = '新闻' + (newsArr.length+1)
this.setState({newsArr: [news, ...newsArr]})
}, 1000)
}

getSnapshotBeforeUpdate() {
return this.refs.list.scrollHeight
}

componentDidUpdate(preProps, preState, height) {
this.refs.list.scrollTop += this.refs.list.scrollHeight - height
}

render() {
return(
<div className="newsList" ref="list">
{this.state.newsArr.map((n, index) => {
return <div key={index} className="news">{n}</div>
})}
</div>
)
}
}

ReactDOM.render(<NewsList/>, document.getElementById('test'))

GIF 2022-3-20 20-36-43

总结新版生命周期的三个阶段

1.初始化阶段: 由ReactDOM.render()触发—初次渲染

  • constructor()
  • getDerivedStateFromProps
  • render()
  • componentDidMount() ====> 常用
    一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息

2.更新阶段: 由组件内部this.setSate()或父组件重新render触发

  • getDerivedStateFromProps
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate
  • componentDidUpdate()

3.卸载组件: 由ReactDOM.unmountComponentAtNode()触发

  • componentWillUnmount() ====> 常用
    一般在这个钩子中做收尾的事,例如:关闭定时器、取消订阅

重要的钩子

  • render:初始化渲染或更新渲染调用
  • componentDidMount:开启监听, 发送ajax请求
  • componentWillUnmount:做一些收尾工作, 如: 清理定时器

即将废弃的钩子

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

现在使用会出现警告,下一个大版本需要加上UNSAFE_前缀才能使用,以后可能会被彻底废弃,不建议使用。

DOM的Diffing算法

当对比两棵树时,React 首先比较两棵树的根节点。不同类型的根节点元素会有不同的形态。

Diffing最小的粒度是标签。

“粒度”表示的是精确程度问题。粗粒度角度描述一个系统,是关注系统中大的组件;细粒度角度描述一个系统是从组成大组件的小组件,或者更小组件的角度认识系统。

系统功能一般又分为多个模块,大的功能又会分为若干模块或者步骤,粒度一步一步细化,直到最终的某个用户操作(输入内容,下拉选择,上传文件,点击按钮等),具体的功能最终得到实现。这是一个粒度由粗到细的过程。反之就是由细到粗。
再从代码设计的角度来说。代码的结构是由类型之间的关联起来的系统。系统的整体结构(架构)就是系统最粗的粒度,代码也同系统功能一样,也是有模块划分(可能类似功能结构划分,也可能有所区别)。那么从复杂结构代码模块,到其细小的组成部分就是粒度由粗到细的过程。

验证Diffing算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Time extends React.Component {
state = {date: new Date()}

componentDidMount() {
setInterval(() => {
this.setState({date: new Date()})
}, 1000)
}

render() {
return (
<div>
<h1>Hello</h1>
<input type="text"/>
<span>
现在是:{this.state.date.toTimeString()}
<input type="text"/>
</span>
</div>
)
}
}

ReactDOM.render(<Time/>, document.getElementById('test'))

GIF 2022-3-20 21-00-40

两个输入框中的输入内容均没有丢失,证明Diffing算法是真实存在的

虚拟DOM中key的作用

简单地说:key是虚拟DOM对象的标识,在更新显示时key起着极其重要的作用

详细的说:当状态中的数据发生变化时,react会根据新数据生成新的虚拟DOM,随后react进行新虚拟DOM与旧虚拟DOM的diff比较,规则如下:

  1. 旧虚拟DOM中找到了与新虚拟DOM相同的key:
    • 若虚拟DOM中内容未变,直接使用之前的真实DOM
    • 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
  2. 旧虚拟DOM中未找到与新虚拟DOM相同的key:
    • 根据数据创建新的真实DOM,随后渲染到页面

用index作为key可能会引发的问题

  1. 若对数据进行:逆序添加、逆序删除等破坏顺序的操作:
    • 会产生没有必要的真实DOM更新 ===> 界面效果没问题,但效率低
  2. 如果结构中包含输入类的DOM:
    • 会产生错误DOM更新 ===> 界面有问题

注意:如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key没有问题

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
class Person extends React.Component {
state = {
persons:[
{id:1, name:'小张', age:18},
{id:2, name:'小王', age:20}
]
}

add = () => {
const {persons} = this.state
const p = {id:persons.length+1, name:'小李', age:35}
this.setState({persons: [p, ...persons]})
}

render() {
return(
<div>
<h2>展示人员信息</h2>
<h3>使用index(索引值)作为key</h3>
<button onClick={this.add}>添加一个小李</button>
<ul>
{
this.state.persons.map((personObj, index) => {
return <li key={index}>{personObj.name}----{personObj.age}<input type="text"/></li>
})
}
</ul>
<hr/>
<h3>使用obj.id(数据的唯一标识)作为key</h3>
<ul>
{
this.state.persons.map((personObj) => {
return <li key={personObj.id}>{personObj.name}----{personObj.age}<input type="text"/></li>
})
}
</ul>
</div>
)
}
}

ReactDOM.render(<Person/>, document.getElementById('test'))

GIF 2022-3-20 22-40-32

开发中如何选择key

  1. 最好使用每条数据的唯一标识作为key,比如id、手机号、学号、身份证号等
  2. 如果确定只是简单的展示数据,使用index亦可