第二章-React面向组件编程(二)
- [七. 组件的生命周期](#七. 组件的生命周期)
-
- [1. 引出生命周期](#1. 引出生命周期)
-
- [1.1 效果](#1.1 效果)
- [1.2 创建组件,实现静态页面](#1.2 创建组件,实现静态页面)
- [1.3 卸载组件](#1.3 卸载组件)
- [1.4 透明度初始化](#1.4 透明度初始化)
- [1.5 添加定时器](#1.5 添加定时器)
- [1.6 清除定时器](#1.6 清除定时器)
- [1.7 完整代码](#1.7 完整代码)
- [1.8 总结](#1.8 总结)
- [2. 生命周期流程(旧)](#2. 生命周期流程(旧))
-
- [2.1 理解](#2.1 理解)
- [2.2 生命周期流程图(旧)](#2.2 生命周期流程图(旧))
- [3. 生命周期(旧)组件挂载流程](#3. 生命周期(旧)组件挂载流程)
- [4. 生命周期(旧)更新流程](#4. 生命周期(旧)更新流程)
-
- [4.1 生命周期(旧)setState 流程](#4.1 生命周期(旧)setState 流程)
-
- [4.1.1 shouldComponentUpdate](#4.1.1 shouldComponentUpdate)
- [4.1.2 更新过程](#4.1.2 更新过程)
- [4.2 生命周期(旧)forceUpdate 流程](#4.2 生命周期(旧)forceUpdate 流程)
- [4.3 生命周期(旧)父组件 render 流程](#4.3 生命周期(旧)父组件 render 流程)
-
- [4.3.1 效果](#4.3.1 效果)
- [4.3.2 componentWillReceiveProps](#4.3.2 componentWillReceiveProps)
- [4.3.3 更新流程](#4.3.3 更新流程)
- [4.4 总结](#4.4 总结)
- [5. 对比新旧生命周期](#5. 对比新旧生命周期)
-
- [5.1 UNSAFE_](#5.1 UNSAFE_)
- [5.2 官网中的说法](#5.2 官网中的说法)
- [5.3 组件生命周期的新旧对比](#5.3 组件生命周期的新旧对比)
- [6. getDerivedStateFromProps](#6. getDerivedStateFromProps)
-
- [6.1 static getDerivedStateFromProps](#6.1 static getDerivedStateFromProps)
- [6.2 返回状态对象](#6.2 返回状态对象)
- [6.3 派生state](#6.3 派生state)
- [7. getSnapshotBeforeUpdate](#7. getSnapshotBeforeUpdate)
-
- [7. 1 返回快照值](#7. 1 返回快照值)
- [7. 2 componentDidUpdate 接受的三个参数](#7. 2 componentDidUpdate 接受的三个参数)
- [7. 3 使用场景和用处](#7. 3 使用场景和用处)
- [7. 4 案例](#7. 4 案例)
- [8. 总结生命周期(新)](#8. 总结生命周期(新))
- [八. DOM的Diffing算法](#八. DOM的Diffing算法)
-
- [1. 什么是Diffing算法](#1. 什么是Diffing算法)
- [2. 验证Diffing算法](#2. 验证Diffing算法)
- [3. key的作用](#3. key的作用)
-
- [3.1 Person案例](#3.1 Person案例)
- [3.2 经典面试题](#3.2 经典面试题)
- [3.3 虚拟DOM中key的作用](#3.3 虚拟DOM中key的作用)
- [3.4 慢动作回放 ------ 使用 index 索引值作为key](#3.4 慢动作回放 —— 使用 index 索引值作为key)
- [3.5 慢动作回放 ------ 使用 id 唯一标识作为key](#3.5 慢动作回放 —— 使用 id 唯一标识作为key)
- [3.6 用index作为key引发的问题](#3.6 用index作为key引发的问题)
- [3.7 开发中如何选择key](#3.7 开发中如何选择key)
七. 组件的生命周期
1. 引出生命周期
1.1 效果
需求:定义组件实现以下功能。
- 让指定文本的透明度从
1到0进行变化(从1逐渐变成0,然后再直接变到到重新开始,以此类推); - 从完全可见(透明度为
1),到彻底消失(透明度为0),耗时2s; - 点击 "
不活了" 按钮,从界面中卸载组件;

1.2 创建组件,实现静态页面
javascript
// 1.创建组件
class Life extends React.Component {
render() {
return (
<>
<h2>react学不会,怎么办?</h2>
<button>不活了</button>
</>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Life/>, document.getElementById("test"))
1.3 卸载组件
我们先从简单的需求开始做,也就是点击按钮"不活了",能把组件从页面上卸载。
unmountComponentAtNode (来自react-dom) 用于从指定的 DOM 节点中移除已挂载的 React 组件,清除其状态和事件监听器。
javascript
// 1.创建组件
class Life extends React.Component {
die() {
// 卸载组件
ReactDOM.unmountComponentAtNode(document.getElementById("test"))
}
render() {
return (
<>
<h2>react学不会,怎么办?</h2>
<button onClick={this.die}>不活了</button>
</>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Life/>, document.getElementById("test"))
可以看到页面的效果

点击按钮之后,组件从页面上移除了!

接下来我们来处理文字透明度的变化。
1.4 透明度初始化
状态中的数据驱动着页面的更新和显示,所以我们把透明度的值放在状态中定义。
javascript
// 1.创建组件
class Life extends React.Component {
// 透明度初始化
state = {opacity: 1}
die() {
// 卸载组件
ReactDOM.unmountComponentAtNode(document.getElementById("test"))
}
render() {
return (
<>
<h2 style={{opacity: this.state.opacity}}>react学不会,怎么办?</h2>
<button onClick={this.die}>不活了</button>
</>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Life/>, document.getElementById("test"))
1.5 添加定时器
- 首先,我们完成定时器的逻辑,然后我们来思考下,应该将这段代码放在哪里?
javascript
setInterval(() => {
let {opacity} = this.state;
opacity -= 0.1;
if (opacity <= 0) {
opacity = 1;
}
this.setState({opacity})
}, 200)
- 添加在
render函数里;
javascript
render() {
setInterval(() => {
let {opacity} = this.state;
opacity -= 0.1;
if (opacity <= 0) {
opacity = 1;
}
this.setState({opacity})
}, 200)
}
我们运行代码,发现将定时器添加在redner里面,除了第1s字体变淡是正常的,后面频率开始逐渐加快,最后字体直接是闪烁状态的,且CPU的温度是一路飙升上去的。
我们知道render会在初始化 和状态更新时 调用,我们把定时器放在render里面且定时器包含了更改状态的动作,更改状态导致另一个定时器被开启,这样引发了无限循环的递归。
- componentDidMount:在组件挂载后立即调用,仅执行一次;
javascript
// 组件挂载完毕
componentDidMount() {
setInterval(() => {
let {opacity} = this.state;
opacity -= 0.1;
if (opacity <= 0) {
opacity = 1;
}
this.setState({opacity})
}, 200)
}
刷新页面,我们看到现在已经完成了文字一点一点变淡的效果。

1.6 清除定时器
现在有一个问题,点击按钮"不活了",组件确实被卸载了,但是控制台出现了一个报错:

这是由于后期卸载了组件,但是定时器仍然存在在页面上所导致的,所以我们需要清除定时器。
- componentWillUnmount :在组件卸载及销毁之前直接调用;
javascript
// 组件挂载完毕
componentDidMount() {
this.timer = setInterval(() => {
let {opacity} = this.state;
opacity -= 0.1;
if (opacity <= 0) {
opacity = 1;
}
this.setState({opacity})
}, 200)
}
// 组件将要卸载
componentWillUnmount() {
// 清除定时器
clearInterval(this.timer);
}
1.7 完整代码
javascript
// 1.创建组件
class Life extends React.Component {
state = {opacity: 1}
die() {
// 卸载组件
ReactDOM.unmountComponentAtNode(document.getElementById("test"))
}
// 组件挂载完毕
componentDidMount() {
this.timer = setInterval(() => {
let {opacity} = this.state;
opacity -= 0.1;
if (opacity <= 0) {
opacity = 1;
}
this.setState({opacity})
}, 200)
}
// 组件将要卸载
componentWillUnmount() {
// 清除定时器
clearInterval(this.timer);
}
// 初始化渲染,状态更新之后
render() {
return (
<>
<h2 style={{opacity: this.state.opacity}}>react学不会,怎么办?</h2>
<button onClick={this.die}>不活了</button>
</>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Life/>, document.getElementById("test"))
1.8 总结
组件的生命周期其实就是人的这一生,在关键的时间点人去做一些事;react 的生命周期就是,在关键的点,我帮你去调一些特殊的函数,让你在函数里面完成一些特殊的事情。

关于生命周期有一些说法,这些说法其实全都是一个意思。
生命周期回调函数 <=> 生命周期钩子函数 <=> 生命周期函数 <=> 生命周期钩子
2. 生命周期流程(旧)
2.1 理解
- 组件从创建到死亡它会经历一些特定的阶段;
- React 组件中包含一系列钩子函数(生命周期回调函数),会在特定的时刻调用;
- 我们在定义组件时,会在特定的生命周期回调函数中,做特定的工作;
2.2 生命周期流程图(旧)

3. 生命周期(旧)组件挂载流程
案例:点击按钮,页面数字 + 1。
javascript
// 1.创建组件
class Count extends React.Component {
// 构造器
constructor(props) {
super(props);
// 初始化状态
this.state = {count: 0}
console.log('Count-----constructor')
}
// 加1按钮的回调
add = () => {
let {count} = this.state;
this.setState({count: count + 1})
}
// 组件将要挂载的钩子
componentWillMount(){
console.log('Count-----componentWillMount')
}
// 组件挂载完毕的钩子
componentDidMount(){
console.log('Count-----componentDidMount')
}
render() {
console.log('Count-----render')
return (
<>
<h2>当前求和为:{this.state.count}</h2>
<button onClick={this.add}>点我+1</button>
</>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Count/>, document.getElementById("test"))
我们看到按照生命周期流程图左边的顺序,控制台依次输出了:constructor ------> componentWillMount ------> render ------> componentDidMount。


此时,我们在页面上再添加一个卸载组件的按钮以及对应的生命周期钩子。
javascript
// 卸载组件按钮的回调
die = () => {
ReactDOM.unmountComponentAtNode(document.getElementById("test"))
}
// 组件将要卸载的钩子
componentWillUnmount() {
console.log('Count-----componentWillUnmount')
}
render() {
console.log('Count-----render')
return (
<>
<h2>当前求和为:{this.state.count}</h2>
<button onClick={this.add}>点我+1</button
<button onClick={this.die}>卸载</button>
</>
)
}
打开控制台,挂载流程一切正常:

点击卸载按钮,我们看到React执行了卸载 对应的生命周期钩子componentWillUnmount:

4. 生命周期(旧)更新流程
右边的生命周期更新流程分为三条线:

4.1 生命周期(旧)setState 流程
4.1.1 shouldComponentUpdate
shouldComponentUpdate:组件是否应该被更新,可以把它理解为一个 "阀门"。
它在组件接收到新的 props 或 state 时被调用,用来决策是否继续后面的流程(即更新组件)。如果返回 true,表示组件应当更新;如果返回 false,本次更新的动作到这里就结束了,不能继续后面的流程了。
- 返回
false:
javascript
// 控制组件更新的"阀门"
shouldComponentUpdate(){
console.log('Count-----shouldComponentUpdate')
return false;
}
阀门就此关闭,更新流程不会再走下去了:

- 返回
true:
javascript
// 控制组件更新的"阀门"
shouldComponentUpdate(){
console.log('Count-----shouldComponentUpdate')
return true;
}
每一次执行更新之前,都会先询问shouldComponentUpdate:

注意: 这个钩子如果不写,底层也会补一个默认返回值,为true;如果你写了这个钩子,那你必须亲自写一个返回值,返回值必须是布尔值。布尔值为true,阀门开启可以走,布尔值为false,阀门关闭不可以走。
4.1.2 更新过程
我们根据流程图,将后面的两个生命周期函数补全。
javascript
// 控制组件更新的"阀门"
shouldComponentUpdate(){
console.log('Count-----shouldComponentUpdate')
return true;
}
// 组件将要更新的钩子
componentWillUpdate(){
console.log('Count-----componentWillUpdate')
}
// 组件更新完毕的钩子
componentDidUpdate(){
console.log('Count-----componentDidUpdate')
}
下面就是一个setState流程更新的过程:

4.2 生命周期(旧)forceUpdate 流程
forceUpdate :强制更新,绕过"阀门 "直接进入到更新流程(不受"阀门"控制);
javascript
// 强制更新按钮的回调
force = () =>{
this.forceUpdate();
}
render() {
console.log('Count-----render')
return (
<>
<h2>当前求和为:{this.state.count}</h2>
<button onClick={this.add}>点我+1</button>
<button onClick={this.die}>卸载</button>
<button onClick={this.force}>不更改状态中的任何数据,强制更新一下</button>
</>
)
}
点击"强制更新 "按钮,我们看到forceUpdate函数触发了下面的更新流程:

即使关掉控制组件更新的"阀门 ",也可以正常的触发更新的流程(但是正常的setState更新走不下去了):
javascript
// 控制组件更新的"阀门"
shouldComponentUpdate(){
console.log('Count-----shouldComponentUpdate')
return false;
}

4.3 生命周期(旧)父组件 render 流程
4.3.1 效果
- 需求 :定义组件
A和 组件B,且A是B的父组件,点击"换车"按钮,更换汽车品牌。

- 代码如下:
javascript
<script type="text/babel">
// 定义 A 组件
class A extends React.Component {
// 初始化状态
state = {carName: '宝马'}
changeCar = () => {
this.setState({carName: '奔驰'})
}
render() {
return (
<>
<h2>我是A组件,
<button onClick={this.changeCar}>换车</button>
</h2>
<B carName={this.state.carName}/>
</>
)
}
}
// 定义 B 组件
class B extends React.Component {
render() {
return (
<>
<h4>我是B组件,接收到的车是:{this.props.carName}</h4>
</>
)
}
}
ReactDOM.render(<A/>, document.getElementById("test"))
</script>
4.3.2 componentWillReceiveProps
- componentWillReceiveProps :在初始化
render的时候不执行,当子组件接受到新的props时才会被触发,一般用于父组件状态更新时子组件的重新渲染。
首先,我们在B组件中,添加这个钩子:
javascript
// 定义 B 组件
class B extends React.Component {
// 组件将要接收新的props的钩子
componentWillReceiveProps() {
console.log('B-----componentWillReceiveProps');
}
render() {
return (
<>
<h4>我是B组件,接收到的车是:{this.props.carName}</h4>
</>
)
}
}
按道理来讲,B组件已经成功接收到props并展示,所以componentWillReceiveProps是一定会被调用的,但是实际上并没有调用:

这是为什么呢?这是因为对于第一次接收的props不算,以后接收的才算。所以在网上有很多人建议把这个钩子的名字改为componentWillReceiveNewProps。
- 接下来点击"
换车"按钮,那么A组件的state被更改,导致A组件render,B组件也随之更新。对于第二次接收到props,componentWillReceiveProps这次会被成功调用。

而且componentWillReceiveProps这个钩子还可以接受参数,参数就是我们传入子组件的props:
javascript
// 组件将要接收新的props的钩子
componentWillReceiveProps(props) {
console.log('B-----componentWillReceiveProps', props);
}

4.3.3 更新流程
我们把后面的钩子全部写一下,来验证下整个流程。
- 完整代码:
javascript
<script type="text/babel">
class A extends React.Component {
// 初始化状态
state = {carName: '宝马'}
changeCar = () => {
this.setState({carName: '奔驰'})
}
render() {
console.log('A-----render');
return (
<>
<h2>我是A组件,
<button onClick={this.changeCar}>换车</button>
</h2>
<B carName={this.state.carName}/>
</>
)
}
}
class B extends React.Component {
// 组件将要接收新的props的钩子
componentWillReceiveProps(props) {
console.log('B-----componentWillReceiveProps', props);
}
// 控制组件更新的"阀门"
shouldComponentUpdate() {
console.log('B-----shouldComponentUpdate');
return true;
}
// 组件将要更新的钩子
componentWillUpdate() {
console.log('B-----componentWillUpdate');
}
// 组件更新完毕的钩子
componentDidUpdate() {
console.log('B-----componentDidUpdate');
}
render() {
console.log('B-----render');
return (
<>
<h4>我是B组件,接收到的车是:{this.props.carName}</h4>
</>
)
}
}
ReactDOM.render(<A/>, document.getElementById("test"))
</script>
</body>
</html>
我们可以看到,控制台输出的钩子函数是和我们流程图上一致的。

4.4 总结
生命周期的三个阶段(旧):
- 初始化阶段 :由
ReactDOM.render()触发 --- 初次渲染;
1.constructor()
2.componentWillMount()
3.render():必须使用
4.componentDidMount() :常用,一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息;
- 更新阶段 :由组件内部
this.setState()或父组件重新render触发;
1.shouldComponentUpdate()
2.componentWillUpdate()
3.render():必须使用
4.componentDidUpdate()
- 卸载组件 :由
ReactDOM.unmountComponentAtNode()触发:
1.componentWillUnmount() :常用,一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息;
5. 对比新旧生命周期
5.1 UNSAFE_
当React版本升至17.0以上,有几个钩子会导致控制台出现警告:

我们按照提示在componentWillMount钩子和componentWillUpdate钩子前面添加UNSAFE_:
javascript
// 组件将要挂载的钩子
UNSAFE_componentWillMount() {
console.log('Count-----componentWillMount')
}
// 组件将要更新的钩子
UNSAFE_componentWillUpdate(){
console.log('Count-----componentWillUpdate')
}
再来看下控制台的情况,我们发现刚才的警告已经消失了:

在新版本中,共有三个钩子需要添加前缀UNSAFE_:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
补充:cdn资源下载地址
5.2 官网中的说法

5.3 组件生命周期的新旧对比

总结:
- 废弃3个旧的钩子:
componentWillMount,componentWillReceiveProps,componentWillUpdate; - 提出2个新的钩子:
getDerivedStateFromProps,getSnapshotBeforeUpdate;
6. getDerivedStateFromProps
6.1 static getDerivedStateFromProps
- getDerivedStateFromProps :从
props获取派生状态。
我们还以Count组件为案例,将要废弃的两个钩子删掉,添加新的getDerivedStateFromProps钩子:
javascript
getDerivedStateFromProps() {
console.log('new-----getDerivedStateFromProps')
}
控制台出现了报错:

我们在前面添加static,把getDerivedStateFromProps作为静态方法使用:
javascript
static getDerivedStateFromProps() {
console.log('new-----getDerivedStateFromProps')
}
这次控制台又报了另外一个错,提示我们应该返回一个state对象或者null:

我们先返回null,看一下效果:
javascript
static getDerivedStateFromProps() {
console.log('new-----getDerivedStateFromProps')
return null;
}
这次控制台果然不报错了,而且顺序和生命周期的流程图也保持一致:constructor ------> getDerivedStateFromProps ------> render------>componentDidMount。

6.2 返回状态对象
返回状态对象可以指定状态里的值,但是状态更新会被影响。
javascript
static getDerivedStateFromProps() {
console.log('new-----getDerivedStateFromProps')
// return null;
return {count: 108}
}
无论怎么点击 +1 按钮,页面都不会更新,值都不会发生改变。

6.3 派生state
getDerivedStateFromProps接受两个参数props和state。
- 我们先看下第一个参数
props:
javascript
static getDerivedStateFromProps(props) {
console.log('new-----getDerivedStateFromProps', props)
// return null;
return {count: 108}
}
...
ReactDOM.render(<Count count={199}/>, document.getElementById("test"))

我们发现props的格式和state对象格式一致,如果这个地方返回props呢?
javascript
static getDerivedStateFromProps(props) {
console.log('new-----getDerivedStateFromProps',props)
return props
}
页面被成功设置为props中的count:

这种通过props得到state的方式,就叫做 派生的状态 。
适用场景:此方法适用于罕见的用例,即 state 的值在任何时候都取决于 props。
- 再来看下第二个参数
state:
javascript
// 若state的值在任何时候都取决于 props,那么可以使用 getDerivedstateFromProps
static getDerivedStateFromProps(props, state) {
console.log('new-----getDerivedStateFromProps', props, state)
return props
}
state为初始化的值0,且count值依然为props中的199:

7. getSnapshotBeforeUpdate
- getSnapshotBeforeUpdate:在更新之前,获取快照。

我们观察一下新的生命周期图,getSnapshotBeforeUpdate这个钩子的上游是render,下游是componentDidUpdate,所以我们这次研究的是更新的过程。
7. 1 返回快照值
首先,我们来验证一下这个钩子执行的时机,看看它是否介于render和componentDidUpdate之间:
javascript
// 在更新之前,获取快照
getSnapshotBeforeUpdate() {
console.log('new-----getSnapshotBeforeUpdate')
}
点击+1按钮,我们看下控制台:

这个钩子执行的时机没有问题:render ------> getSnapshotBeforeUpdate ------> componentDidUpdate,但是控制台报了"没有返回快照值"的警告,那么我们返回null再来看下效果:
javascript
// 在更新之前,获取快照
getSnapshotBeforeUpdate() {
console.log('new-----getSnapshotBeforeUpdate')
return null
}
控制台警告消失:

但是如果不返回null呢?
如果不返回null,可以返回任何值作为快照值,例如:字符串,数字,布尔值,甚至是函数:
javascript
// 在更新之前,获取快照
getSnapshotBeforeUpdate() {
console.log('new-----getSnapshotBeforeUpdate')
return 'abc'
}
但是返回的 快照值 交给谁了?它又有什么作用呢?让我们接下来探讨下componentDidUpdate钩子。
7. 2 componentDidUpdate 接受的三个参数
javascript
componentDidUpdate(prevProps, prevState, snapshot)
- prevProps :之前的
props; - prevState :之前的
state; - snapshot :
getSnapshotBeforeUpdate返回的快照值;
javascript
// 组件更新完毕的钩子
componentDidUpdate(prevProps, prevState, snapshotValue) {
console.log('Count-----componentDidUpdate', prevProps, prevState, snapshotValue)
}

7. 3 使用场景和用处
getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如:滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()。
此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。
7. 4 案例
- 了解两个基础属性
scrollHeight和scrollTop
我们先来完成一个简单的新闻列表,新闻一共7 条,最多展示5条,剩余需要滚动才能展示。
css代码:
javascript
<style>
.newsList {
width: 200px;
height: 150px;
background-color: skyblue;
overflow: auto;
}
li {
height: 30px;
line-height: 30px;
}
</style>
js代码:
javascript
<div class="newsList" id="newsList">
<li>新闻7</li>
<li>新闻6</li>
<li>新闻5</li>
<li>新闻4</li>
<li>新闻3</li>
<li>新闻2</li>
<li>新闻1</li>
</div>
页面效果:

- scrollTop:用于获取或设置元素内容垂直滚动的像素数,它表示元素可视区域顶部距离其内容实际顶部的距离;
如果现在我们想要新闻6打头,需要将newslist容器向上滚动30px(每个li的高度是30px):
javascript
const newsList = document.getElementById("newsList");
newsList.scrollTop = 30;

如果想要新闻5打头:
javascript
const newsList = document.getElementById("newsList");
newsList.scrollTop = 60;

- scrollHeight:获取元素内容的总高度;
javascript
const newsList = document.getElementById("newsList");
console.log(newsList.scrollHeight); // 210
- 将上面案例改写成类式组件的方式
javascript
class NewsList extends React.Component {
render() {
return (
<div className="newsList" ref={c => this.list = c}>
<li>新闻7</li>
<li>新闻6</li>
<li>新闻5</li>
<li>新闻4</li>
<li>新闻3</li>
<li>新闻2</li>
<li>新闻1</li>
</div>
)
}
}
ReactDOM.render(<NewsList/>, document.getElementById("test"))
- 我们想让初始化的时候列表为空,然后每隔
1s在上方插入一条新闻:
javascript
class NewsList extends React.Component {
state = {list: []}
render() {
return (
<div className="newsList">
{
this.state.list.map((item, i) => {
return <li key={i}>{item}</li>
})
}
</div>
)
}
componentDidMount() {
setInterval(() => {
// 获取原状态
let {list} = this.state;
// 模拟一条新闻
let news = `新闻${list.length + 1}`;
// 更新状态
this.setState({list: [news, ...list]})
}, 1000)
}
}
ReactDOM.render(<NewsList/>, document.getElementById("test"))

- 当我滚动下拉条到想看的新闻,比如
新闻6,即使上面在不停的新增新闻,滚动条也能一直在这个地方停住,让我浏览:

可以使用getSnapshotBeforeUpdate获取之前内容高度的快照,在每次更新之前对scrollTop重新设置,新增新闻后内容高度相差多少,就让scrollTop再向上滚动多少:
javascript
getSnapshotBeforeUpdate() {
return this.list.scrollHeight;
}
componentDidUpdate(prevProps, prevState, height) {
this.list.scrollTop += this.list.scrollHeight - height
}
这样就能实现我们想要的效果啦!
8. 总结生命周期(新)
生命周期的三个阶段(新):
- 初始化阶段 :由
ReactDoM.render()触发--- 初次渲染;
- constructor()
- getDerivedstateFromProps()
- render()
- componentDidMount() --- 常用
一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息;
- 更新阶段 :由组件内部
this.setState()或父组件重新render触发;
- getDerivedstateFromProps
- shouldcomponentUpdate()
- render()
- getsnapshotBeforeUpdate()
- componentDidUpdate()
.
- 卸载组件 :由
ReactDOM.unmountComponentAtNode()触发;
- componentWillUnmount() --- 常用
一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
八. DOM的Diffing算法
1. 什么是Diffing算法
Diffing算法是一种用于比较两棵树形结构(通常是虚拟DOM树 )并找出最小差异的高效算法,广泛用于React和Vue等前端框架。它通过同层比较(Tree Diff)、组件复用判断(Component Diff)和元素Key值匹配(Element Diff)来最小化DOM操作,从而实现局部更新并提升渲染性能。
a.核心策略与工作原理:
-
同层比较(Tree Diff): 算法只对虚拟DOM树进行同层级的比较,如果节点类型不同,直接销毁旧节点并创建新节点。
-
组件比较(Component Diff) : 若组件类型相同,则仅更新组件的属性(
Props)和状态(State)。 -
Key值匹配(Element Diff) : 在同一层级比较子节点时,通过唯一的
key属性识别节点变化(新增、删除、移动),从而复用未变化的节点。 -
时间复杂度优化 : 通过这些策略,
Diffing算法将O(n³)的复杂度降低为O(n),极大提升了UI更新效率。
b.主要应用场景:
- React/Vue 虚拟DOM更新: 当数据变化时,计算出新的虚拟DOM树与旧树的差异,将变化应用于真实DOM。
- 增量数据处理: 在需要只传输或应用变化数据的场景中计算增量。
2. 验证Diffing算法
我们来看下面的这个Time组件,每隔1s都会更新一次时间,显示当前时间。
- 效果:

- 代码:
javascript
class Time extends React.Component {
state = {date: new Date()}
componentDidMount() {
setInterval(() => {
this.setState({date: new Date()})
}, 1000)
}
render() {
return (
<>
<h3>Hello</h3>
<div>
<input type="text"/>
<span>现在是:{this.state.date.toTimeString()}</span>
</div>
</>
)
}
}
ReactDOM.render(<Time/>, document.getElementById("test"))
Diffing算法的比较过程: 当过了1s后,date变成了新的时间,需要重新调用render,此时就需要和之前的虚拟DOM进行比较,然后发现蓝框里面的DOM元素没有变化,变化的只有红框里面的span标签。所以 React 会直接复用蓝框里面的真实DOM,只需要重新生成红框里面的DOM元素即可。

我们来验证一下React 是否会直接复用蓝框里面的真实DOM。
在input输入框里面输入123,如果input也需要每隔1s重新生成的话,那么输入的内容一定会丢失:


事实证明,时间在更新,但是输入的内容还在,所以也强有力的证明了除了span元素外的其他元素,使用的还是之前的真实DOM。
3. key的作用
3.1 Person案例
我们想实现一个人员列表,点击添加按钮,在列表最上方添加一个"小王"的人员信息。
- 代码:
javascript
// 1.创建组件
class Person extends React.Component {
// 列表数据
state = {list: [{id: 1, name: '小何', age: 18}, {id: 2, name: '小隋', age: 19}]}
// 添加一个小王
add = () => {
let {list} = this.state;
let personObj = {id: list.length + 1, name: '小王', age: 20}
this.setState({list: [personObj, ...list]})
}
render() {
return (
<>
<h2>展示人员信息</h2>
<button onClick={this.add}>添加一个小王</button>
<ul>
{
this.state.list.map((personObj, i) => {
return <li key={i}>{personObj.name}-----{personObj.age}</li>
})
}
</ul>
</>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Person/>, document.getElementById("test"));
- 效果:

添加小王后:

页面看似功能都已经实现了,殊不知在这里面有一个非常严重的效率问题。
3.2 经典面试题
- react/vue 中的
key有什么作用?(key的内部原理是什么?) - 为什么遍历列表时,
key最好不要用index?
3.3 虚拟DOM中key的作用
-
简单的说:
key是虚拟DOM对象的标识,在更新显示时key起着极其重要的作用。 -
详细的说:当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】随后React进行【新虚拟DOM】与【旧虚拟DOM】的
diffing比较,比较规则如下:1.旧虚拟DOM中找到了与新虚拟DOM相同的
key:(a) 若虚拟DOM中内容没变,直接使用之前的真实DOM; (b) 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM;2.旧虚拟DOM中未找到与新虚拟DOM相同的
key:根据数据创建新的真实DOM,随后渲染到到页面。
3.4 慢动作回放 ------ 使用 index 索引值作为key
- 初始数据:
javascript
{id: 1, name: '小何', age: 18},
{id: 2, name: '小隋', age: 19}
- 初始的虚拟DOM:
javascript
<li key=0>小何-----18</li>
<li key=1>小隋-----19</li>
- 更新后的数据:
javascript
{id: 3, name: '小王', age: 20},
{id: 1, name: '小何', age: 18},
{id: 2, name: '小隋', age: 19}
- 更新数据后的虚拟DOM:
javascript
<li key=0>小王-----20</li>
<li key=1>小何-----18</li>
<li key=2>小隋-----19</li>
diffing算法对比新旧虚拟DOM:
- 找到
key=0的DOM做比较,由于内容变了,需要生成新DOM; - 找到
key=1的DOM做比较,由于内容变了,需要生成新DOM; - 由于未在旧的虚拟DOM中找到
key=2的DOM,则需要直接生成新DOM;
其实明明小何-----18,小隋-----19,这两条数据是可以直接复用的,但是由于我们的错误------使用了索引值作为key ,并且小王还是往前面放的(把索引值的顺序打乱了),所以导致页面本来可以进行1次真实DOM的更新,变成了3次真实DOM的更新。
如果页面有2000条数据,使用index作为key,此时在前方添加1条数据,明明页面上只需要绘制1条真实DOM,但是却变成了需要绘制2001条,所以说这是一个很严重的效率问题。
那么这个问题该怎么样解决呢?
我们采用每一条数据的唯一标识id作为key就可以了。
3.5 慢动作回放 ------ 使用 id 唯一标识作为key
- 初始数据:
javascript
{id: 1, name: '小何', age: 18},
{id: 2, name: '小隋', age: 19}
- 初始的虚拟DOM:
javascript
<li key=1>小何-----18</li>
<li key=2>小隋-----19</li>
- 更新后的数据:
javascript
{id: 3, name: '小王', age: 20},
{id: 1, name: '小何', age: 18},
{id: 2, name: '小隋', age: 19}
- 更新数据后的虚拟DOM:
javascript
<li key=3>小王-----20</li>
<li key=1>小何-----18</li>
<li key=2>小隋-----19</li>
diffing算法对比新旧虚拟DOM:
-
由于未在旧的虚拟DOM中找到
key=3的DOM,则需要直接生成新DOM; -
找到
key=1的DOM做比较,由于内容没变,则直接复用; -
找到
key=2的DOM做比较,由于内容没变,则直接复用;
我们添加了1个人,只引起页面1次的DOM更新,所以用id作为key效率更高。
3.6 用index作为key引发的问题
- 我们在每个
li元素的里面添加一个输入框:
javascript
// 1.创建组件
class Person extends React.Component {
// 列表数据
state = {list: [{id: 1, name: '小何', age: 18}, {id: 2, name: '小隋', age: 19}]}
// 添加一个小王
add = () => {
let {list} = this.state;
let personObj = {id: list.length + 1, name: '小王', age: 20}
this.setState({list: [personObj, ...list]})
}
render() {
return (
<>
<div>
<h2>展示人员信息</h2>
<h3>使用index(索引值)作为key</h3>
<button onClick={this.add}>添加一个小王</button>
<ul>
{
this.state.list.map((personObj, i) => {
return <li key={i}>{personObj.name}----_{personObj.age} <input type="text"/></li>
})
}
</ul>
</div>
<div>
<h2>展示人员信息</h2>
<h3>使用id(唯一标识)作为key</h3>
<button onClick={this.add}>添加一个小王</button>
<ul>
{
this.state.list.map((personObj) => {
return <li key={personObj.id}>{personObj.name}-----{personObj.age} <input type="text"/></li>
})
}
</ul>
</div>
</>
)
}
}
// 2.渲染组件到页面
ReactDOM.render(<Person/>, document.getElementById("test"));
- 在
input输入框里输入对应成员的信息:

- 点击添加按钮:

我们发现使用index作为key的列表数据的错乱的,我们来通过慢动作回放来分析一下数据错乱的原因:
【慢动作回放 ------ 使用 index 索引值作为key】
- 初始数据:
javascript
{id: 1, name: '小何', age: 18},
{id: 2, name: '小隋', age: 19}
- 初始的虚拟DOM:
javascript
<li key=0>小何-----18<input type="text"/></li>
<li key=1>小隋-----19<input type="text"/></li>
- 更新后的数据:
javascript
{id: 3, name: '小王', age: 20},
{id: 1, name: '小何', age: 18},
{id: 2, name: '小隋', age: 19}
- 更新数据后的虚拟DOM:
javascript
<li key=0>小王-----20<input type="text"/></li>
<li key=1>小何-----18<input type="text"/></li>
<li key=2>小隋-----19<input type="text"/></li>
diffing算法对比新旧虚拟DOM:
-
找到
key=0的DOM做比较,发现文本内容发生变化后,继续对比后面输入框,发现输入框相同(虚拟DOM没有value值,只能对比type),则直接复用该输入框,该输入框残存着小何-----18的数据; -
找到
key=1的DOM做比较,发现文本内容发生变化后,继续对比后面输入框,发现输入框相同,则直接复用该输入框,该输入框残存着小隋-----19的数据; -
由于未在旧的虚拟DOM中找到
key=2的DOM,则需要直接生成新DOM;
【慢动作回放 ------ 使用 id 唯一标识作为key】
- 初始数据:
javascript
{id: 1, name: '小何', age: 18},
{id: 2, name: '小隋', age: 19}
- 初始的虚拟DOM:
javascript
<li key=1>小何-----18<input type="text"/></li>
<li key=2>小隋-----19<input type="text"/></li>
- 更新后的数据:
javascript
{id: 3, name: '小王', age: 20},
{id: 1, name: '小何', age: 18},
{id: 2, name: '小隋', age: 19}
- 更新数据后的虚拟DOM:
javascript
<li key=3>小王-----20<input type="text"/></li>
<li key=1>小何-----18<input type="text"/></li>
<li key=2>小隋-----19<input type="text"/></li>
diffing算法对比新旧虚拟DOM:
- 由于未在旧的虚拟DOM中找到
key=3的DOM,则需要直接生成新DOM; - 找到
key=1的DOM做比较,由于内容没变且输入框相同(小何的输入框还是给小何用了),则直接复用; - 找到
key=2的DOM做比较,由于内容没变且输入框相同(小隋的输入框还是给小隋用了),则直接复用;
总结:
- 若对数据进行:逆序添加、逆序删除等破坏顺序 的操作,会产生没有必要的真实DOM更新 ==> 界面效果没问题,但效率低。
- 如果结构中还包含输入类 的DOM:会产生错误DOM更新 ==> 数据错乱,界面有问题。
- 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序的操作, 仅用于渲染列表展示,使用
index作为key是没有问题的。
3.7 开发中如何选择key
- 最好使用每条数据的唯一标识 作为
key,比如id、手机号、身份证号、学号等唯一值。 - 如果确定只是简单的展示数据,用
index也是可以的。