React 学习笔记(三) -- 生命周期与diff算法

1,219 阅读10分钟

GitHub:React 源码+笔记

引言

在 React 中为我们提供了一些生命周期钩子函数,让我们能在 React 执行的重要阶段,在钩子函数中做一些事情。那么在 React 的生命周期中,有哪些钩子函数呢,我们来总结一下

引出生命周期

<!-- 准备好一个“容器” -->
<div id="test"></div>

<!-- 引入react核心库 -->
<script type="text/javascript" src="../js/react.development.js"></script>
<!-- 引入react-dom,用于支持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">
//创建组件
//生命周期回调函数 <=> 生命周期钩子函数 <=> 生命周期函数 <=> 生命周期钩子
class Life extends React.Component{

        state = {opacity:1}

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

        //组件挂完毕
        componentDidMount(){
        //定时器不能放在render中,因为状态恒信会导致render再次调用定时器
                console.log('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(<Life/>,document.getElementById('test'))
</script>

React 生命周期(旧)

图示

image.png

  1. 初始化阶段: 由ReactDOM.render()触发---初次渲染
  • constructor()
  • componentWillMount()
  • render()
  • componentDidMount() =====> 常用 一般在这个钩子中做一些初始化的事.
    例如:开启定时器、发送网络请求、订阅消息
  1. 更新阶段: 由组件内部this.setSate()或父组件render触发
  • shouldComponentUpdate()
  • componentWillUpdate()
  • render() =====> 必须使用的一个
  • componentDidUpdate()
  1. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
  • componentWillUnmount() =====> 常用
    一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

代码示例

<!-- 准备好一个“容器” -->
<div id="test"></div>

<!-- 引入react核心库 -->
<script type="text/javascript" src="../js/react.development.js"></script>
<!-- 引入react-dom,用于支持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">

//创建组件
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');
        }

        //控制组件更新的“阀门”,返回true表示可以更新,返回false表示不能更新
        // 这个钩子如果你不写,react底层会帮你自动补全,并返回true,如果你写了以你写的为主
        shouldComponentUpdate() {
                console.log('Count---shouldComponentUpdate');
                return true
        }

        //组件将要更新的钩子
        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>
                )
        }
}

//父组件A
class A extends React.Component {
        //初始化状态
        state = { carName: '奔驰' }

        changeCar = () => {
                this.setState({ carName: '奥拓' })
        }

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

//子组件B
class B extends React.Component {
        //组件将要接收新的props的钩子,第一次传参不会调用,可以接受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 (
                        <div>我是B组件,接收到的车是:{this.props.carName}</div>
                )
        }
}

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

即将废弃

三个生命周期钩子在 React 18版本中将要被废弃,官方解释是在 React 异步机制下,如果滥用这个钩子可能会有 Bug,所以现在前面要加上UNSAFE_

React 生命周期(新)

图示

image-20210821102622645 React 生命周期主要包括三个阶段:初始化阶段,更新阶段,销毁阶段

过程

初始化

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

更新

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

销毁

  • componentWillUnmount()

初始化阶段

1. constructor 执行

constructor 在组件初始化的时候只会执行一次

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。 constructor中通常只做两件事情:

  • 通过给 this.state 赋值对象来初始化内部的state;
  • 为事件绑定实例(this)
constructor(props) {
    console.log('进入构造器');
    super(props)
    this.state = { count: 0 }
}

现在我们通常不会使用 constructor 属性,而是改用类加箭头函数的方法,来替代 constructor

例如,我们可以这样初始化 state

state = {
	count: 0
};

2. static getDerivedStateFromProps 执行 (新钩子)

返回一个状态对象或null

这个是 React 新版本中新增的2个钩子之一,据说很少用。

getDerivedStateFromProps 在初始化和更新中都会被调用,并且在 render 方法之前调用,它返回一个对象用来更新 state

getDerivedStateFromProps 是类上直接绑定的静态(static)方法,它接收两个参数 propsstate

props 是即将要替代 state 的值,而 state 是当前未替代前的值

注意:state 的值在任何时候都取决于传入的 props ,不会再改变

如下

static getDerivedStateFromProps(props) {
    return props
}
ReactDOM.render(<Count count="109"/>,document.querySelector('.test'))

count 的值不会改变,一直是 109,因为return的为props

//构造器
    constructor(props){
        super(props)
        //初始化状态
        this.state = {count:0}
    }
//若state的值在任何时候都取决于props,那么可以使用getDerivedStateFromProps
    static getDerivedStateFromProps(props,state){
            console.log('getDerivedStateFromProps',props,state);
            return null
    }
   ReactDOM.render(<Count count={199}/>,document.getElementById('test'))

count 的值会改变,因为return的为null

3. render 执行

render() 方法是组件中必须实现的方法,用于渲染 DOM ,但是它不会真正的操作 DOM,它的作用是把需要的东西返回出去。

实现渲染 DOM 操作的是 ReactDOM.render()

注意:避免在 render 中使用 setState ,否则会死循环

4. componentDidMount 执行

componentDidMount 的执行意味着初始化挂载操作已经基本完成,它主要用于组件挂载完成后做某些操作,这个挂载完成指的是:组件插入 DOM tree

componentDidMount中通常进行哪里操作呢

  • 依赖于DOM的操作可以在这里进行;
  • 在此处发送网络请求就最好的地方;(官方建议)
  • 可以在此处添加一些订阅(会在componentWillUnmount取消订阅);

更新生命周期

1. getDerivedStateFromProps 执行

从props中获得派生状态

执行生命周期getDerivedStateFromProps, 返回的值用于合并 state,生成新的state

2. shouldComponentUpdat 执行

shouldComponentUpdate() 在组件更新之前调用,可以通过返回值来控制组件是否更新,允许更新返回 true ,反之不更新

3. render 执行

在控制是否更新的函数中,如果返回 true 才会执行 render ,得到最新的 React element

4. getSnapshotBeforeUpdate 执行

在更新之前获取快照

在最近一次的渲染输出之前被提交之前调用,也就是即将挂载时调用

相当于淘宝购物的快照,会保留下单前的商品内容,在 React 中就相当于是 即将更新前的状态

它可以使组件在 DOM 真正更新之前捕获一些信息(例如滚动位置),此生命周期返回的任何值都会作为参数传递给 componentDidUpdate()。如不需要传递任何值,那么请返回 null

//在更新之前获取快照
    getSnapshotBeforeUpdate() {
            console.log('getSnapshotBeforeUpdate');
            return 'atguigu'
    }
//组件更新完毕的钩子
// componentDidUpdate(先前传递的Props,先前传递的State,getSnapshotBeforeUpdate的返回值)
    componentDidUpdate(preProps, preState, snapshotValue) {
          console.log('Count---componentDidUpdate', preProps, preState, snapshotValue);
    }
实例

控制滚动条固定在滑动到的位置,即使有新数据也不会自动滑动跳转到顶部 image.png

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>4_getSnapShotBeforeUpdate的使用场景</title>
    <style>
        .list {
                width: 200px;
                height: 150px;
                background-color: skyblue;
                overflow: auto;
        }

        .news {
                height: 30px;
        }
    </style>
</head>

<body>
<!-- 准备好一个“容器” -->
<div id="test"></div>

<!-- 引入react核心库 -->
<script type="text/javascript" src="../js/17.0.1/react.development.js"></script>
<!-- 引入react-dom,用于支持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>

<script type="text/babel">
    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
        }
        // scrollTop滚动区内容上移的高度, scrollHeight内容区高度
        componentDidUpdate(preProps, preState, height) {
            // scrollTop滚动区内容上移的高度=上一次scrollTop滚动区内容上移的高度+每次的差值
            this.refs.list.scrollTop += this.refs.list.scrollHeight - height
        }

        render() {
            return (
                <div className="list" ref="list">
                    {
                        this.state.newsArr.map((n, index) => {
                                return <div key={index} className="news">{n}</div>
                        })
                    }
                </div>
            )
        }
    }
    ReactDOM.render(<NewsList />, document.getElementById('test'))
</script>
</body>
</html>

5. componentDidUpdate 执行

componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。

  • 当组件更新后,可以在此处对 DOM 进行操作;
  • 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。

销毁阶段

componentWillUnmount 执行

componentWillUnmount() 会在组件卸载及销毁之前直接调用。 在此方法中执行必要的清理操作; 例如:

  • 清除 timer,取消网络请求
  • 清除 在 componentDidMount() 中创建的订阅等

React更新机制

1. React的渲染流程

image.png

2. React的更新流程

image.png

React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树。React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI:

  • 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为O(n 3 ),其中n 是树中元素的数量;
  • 如果在React 中使用了该算法,那么展示1000 个元素所需要执行的计算量将在十亿的量级范围;
  • 这个开销太过昂贵了,React的更新性能会变得非常低效; 于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?
  1. 同层节点之间相互比较,不会垮节点比较;
  2. 不同类型的节点,产生不同的树结构;
  3. 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;

情况一:对比不同类型的元素

  • 当节点为不同的元素,React会拆卸原有的树,并且建立起新的树:
  • 当一个元素从<a>变成<img>,从<Article>变成<Comment>,或从<Button>变成<div>都会触发一个完整的重建流程,整颗子树会进行卸载和重建;
  • 当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行componentWillUnmount()方法;
  • 当建立一棵新的树时,对应的DOM 节点会被创建以及插入到DOM 中,组件实例将执行componentWillMount()方法,紧接着componentDidMount()方法; 比如下面的代码更改:React 会销毁Counter组件并且重新装载一个新的组件,而不会对Counter进行比较复用;

image.png

情况二:对比同一类型的元素

  • 当比对两个相同类型的React 元素时,React 会保留DOM 节点,仅比对及更新有改变的属性。
  • 比如下面的代码更改:通过比对这两个元素,React 知道只需要修改DOM 元素上的className属性;

image.png

  • 比如下面的代码更改:当更新style属性时,React 仅更新有所更变的属性。
  • 通过比对这两个元素,React 知道只需要修改DOM 元素上的color样式,无需修改fontWeight。

image.png

  • 如果是同类型的组件元素:组件会保持不变,React会更新该组件的props,并且调用componentWillReceiveProps()和componentWillUpdate()方法;
  • 下一步,调用render()方法,diff 算法将在之前的结果以及新的结果中进行递归;

情况三:对子节点进行递归

在默认条件下,当递归DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个mutation。

  • 我们来看一下在最后插入一条数据的情况:
  • 前面两个比较是完全相同的,所以不会产生mutation;
  • 最后一个比较,产生一个mutation,将其插入到新的DOM树中即可;

image.png

但是如果我们是在中间插入一条数据:

image.png

  • React会对每一个子元素产生一个mutation,而不是保持<li>星际穿越</li><li>盗梦空间</li>的不变;
  • 这种低效的比较方式会带来一定的性能问题,所以要用到key

diffing 算法与key

1. keys的优化

我们在前面遍历列表时,总是会提示一个警告,让我们加入一个key属性:

image.png

  • 方式一:在最后位置插入数据,这种情况,有无key意义并不大
  • 方式二:在前面插入数据这种做法,在没有key的情况下,所有的li都需要进行修改; 当子元素拥有key 时,React 使用key 来匹配原有树上的子元素以及最新树上的子元素:

key的注意事项:

  • key应该是唯一的;
  • key不要使用随机数(随机数在下一次render时,会重新生成一个数字);
  • 使用index作为key,对性能是没有优化的;
/*
经典面试题:
1). react/vue中的key有什么作用?(key的内部原理是什么?)
2). 为什么遍历列表时,key最好不要用index?

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

  2). 详细的说: 当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】, 
      随后React进行【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则如下:
          a. 旧虚拟DOM中找到了与新虚拟DOM相同的key:
              (1).若虚拟DOM中内容没变, 直接使用之前的真实DOM
              (2).若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM

          b. 旧虚拟DOM中未找到与新虚拟DOM相同的key:
                根据数据创建新的真实DOM,随后渲染到到页面

2. 用index作为key可能会引发的问题:
    1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:
           会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。

    2. 如果结构中还包含输入类的DOM:
           会产生错误DOM更新 ==> 界面有问题。

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

3. 开发中如何选择key?:
    1.最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。
    2.如果确定只是简单的展示数据,用index也是可以的。
*/

/* 
慢动作回放----使用index索引值作为key

初始数据:
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},
初始的虚拟DOM:
<li key=0>小张---18<input type="text"/></li>
<li key=1>小李---19<input type="text"/></li>

更新后的数据:
{id:3,name:'小王',age:20},
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},
更新数据后的虚拟DOM:
<li key=0>小王---20<input type="text"/></li>
<li key=1>小张---18<input type="text"/></li>
<li key=2>小李---19<input type="text"/></li>

-----------------------------------------------------------------

慢动作回放----使用id唯一标识作为key

初始数据:
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},
初始的虚拟DOM:
<li key=1>小张---18<input type="text"/></li>
<li key=2>小李---19<input type="text"/></li>

更新后的数据:
{id:3,name:'小王',age:20},
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},
更新数据后的虚拟DOM:
<li key=3>小王---20<input type="text"/></li>
<li key=1>小张---18<input type="text"/></li>
<li key=2>小李---19<input type="text"/></li>
*/
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>key的作用</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 -->
<script type="text/javascript" src="../js/babel.min.js"></script>

<script type="text/babel">

class Person extends React.Component{

        state = {
                persons:[
                        {id:1,name:'小张',age:18},
                        {id:2,name:'小李',age:19},
                ]
        }

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

        render(){
            return (
            <div>
                <h2>展示人员信息</h2>
                <button onClick={this.add}>添加一个小王</button>
                <h3>使用index(索引值)作为key</h3>
                <ul>
                        {
                            this.state.persons.map((personObj,index)=>{
                                    return <li key={index}>{personObj.name}---{personObj.age}<input type="text"/></li>
                                })
                        }
                </ul>
                <hr/>
                <hr/>
                <h3>使用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'))
</script>
</body>
</html>