【第二章-React面向组件编程(二】

第二章-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. 让指定文本的透明度从10进行变化(从1逐渐变成0,然后再直接变到到 重新开始,以此类推);
  2. 从完全可见(透明度为1),到彻底消失(透明度为0),耗时2s
  3. 点击 "不活了" 按钮,从界面中卸载组件;

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 理解

  1. 组件从创建到死亡它会经历一些特定的阶段;
  2. React 组件中包含一系列钩子函数(生命周期回调函数),会在特定的时刻调用;
  3. 我们在定义组件时,会在特定的生命周期回调函数中,做特定的工作;

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:组件是否应该被更新,可以把它理解为一个 "阀门"。

它在组件接收到新的 propsstate 时被调用,用来决策是否继续后面的流程(即更新组件)。如果返回 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,且AB的父组件,点击"换车"按钮,更换汽车品牌。
  • 代码如下
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组件renderB组件也随之更新。对于第二次接收到propscomponentWillReceiveProps这次会被成功调用。

而且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 总结

生命周期的三个阶段(旧):

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

1.constructor()

2.componentWillMount()

3.render():必须使用

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

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

1.shouldComponentUpdate()

2.componentWillUpdate()

3.render():必须使用

4.componentDidUpdate()

  1. 卸载组件 :由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个旧的钩子:componentWillMountcomponentWillReceivePropscomponentWillUpdate
  • 提出2个新的钩子:getDerivedStateFromPropsgetSnapshotBeforeUpdate

6. getDerivedStateFromProps

6.1 static getDerivedStateFromProps

  • getDerivedStateFromPropsprops获取派生状态。

我们还以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接受两个参数propsstate

  • 我们先看下第一个参数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 返回快照值

首先,我们来验证一下这个钩子执行的时机,看看它是否介于rendercomponentDidUpdate之间:

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
  • snapshotgetSnapshotBeforeUpdate返回的快照值;
javascript 复制代码
// 组件更新完毕的钩子
componentDidUpdate(prevProps, prevState, snapshotValue) {
	console.log('Count-----componentDidUpdate', prevProps, prevState, snapshotValue)
}

7. 3 使用场景和用处

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

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

7. 4 案例

  1. 了解两个基础属性 scrollHeightscrollTop

我们先来完成一个简单的新闻列表,新闻一共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
  1. 将上面案例改写成类式组件的方式
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"))
  1. 我们想让初始化的时候列表为空,然后每隔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"))
  1. 当我滚动下拉条到想看的新闻,比如新闻6,即使上面在不停的新增新闻,滚动条也能一直在这个地方停住,让我浏览:

可以使用getSnapshotBeforeUpdate获取之前内容高度的快照,在每次更新之前对scrollTop重新设置,新增新闻后内容高度相差多少,就让scrollTop再向上滚动多少:

javascript 复制代码
getSnapshotBeforeUpdate() {
    return this.list.scrollHeight;
}

componentDidUpdate(prevProps, prevState, height) {
    this.list.scrollTop += this.list.scrollHeight - height
}

这样就能实现我们想要的效果啦!

8. 总结生命周期(新)

生命周期的三个阶段(新):

  1. 初始化阶段 :由ReactDoM.render()触发--- 初次渲染;
  • constructor()
  • getDerivedstateFromProps()
  • render()
  • componentDidMount() --- 常用

一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息;

  1. 更新阶段 :由组件内部this.setState()或父组件重新render触发;
  • getDerivedstateFromProps
  • shouldcomponentUpdate()
  • render()
  • getsnapshotBeforeUpdate()
  • componentDidUpdate()

.

  1. 卸载组件 :由ReactDOM.unmountComponentAtNode()触发;
  • componentWillUnmount() --- 常用

一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

八. DOM的Diffing算法

1. 什么是Diffing算法

Diffing算法是一种用于比较两棵树形结构(通常是虚拟DOM树 )并找出最小差异的高效算法,广泛用于ReactVue等前端框架。它通过同层比较(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引发的问题

  1. 我们在每个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}&nbsp;&nbsp;<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}&nbsp;&nbsp;<input type="text"/></li>
                            })
                        }
                    </ul>
                </div>

            </>
        )
    }
}

// 2.渲染组件到页面
ReactDOM.render(<Person/>, document.getElementById("test"));
  1. input输入框里输入对应成员的信息:
  2. 点击添加按钮:

我们发现使用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也是可以的。
相关推荐
Maimai108083 小时前
React Server Components 是什么?一文讲清 CSR、Server Components 与 Next.js 中的客户端/服务端组件
前端·javascript·css·react.js·前端框架·html·web3
前端进阶之旅19 小时前
React 18 并发特性实战指南:提升大型应用性能的关键技术
前端·react.js·前端框架
Maimai1080820 小时前
Next.js 16 缓存策略详解:从旧模型到 Cache Components
开发语言·前端·javascript·react.js·缓存·前端框架·reactjs
下北沢美食家20 小时前
React面试题
前端·javascript·react.js
有意义20 小时前
极简的React 实现一
前端·javascript·react.js
打小就很皮...20 小时前
实现可交互的泳道图组件(React)
前端·react.js·泳道图
大雷神21 小时前
HarmonyOS APP<玩转React>开源教程四:状态管理基础
react.js·开源·harmonyos
weixin_446260851 天前
提升开发效率的神器!快速选择编码上下文 — React Grab
前端·react.js·前端框架
Csvn1 天前
使用 React Hooks 优化组件性能的 5 个技巧
前端·javascript·react.js