16.React性能优化SCU

2. React性能优化SCU

PureComponent

memo

数据不可变力量

books

this.state.books.splice(2,1)

this.setState({books:this.state.books})

2.1. React更新机制
    1. 在前面已经了解过React的渲染流程:
    • 编写jsx -> babel转换成React.createElement -> ReactElement -> 虚拟DOM(形成树结构) -> 真实DOM
    1. 那么React的更新流程呢?
2.2. React的更新流程
    1. React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树。
    1. React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI:
    • 如何一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法复杂程度为O(n^3),其中n是树中元素的数量;
    • 外链图片转存中...(img-E7dHwjHv-1761926407037)\]https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf

    • 这个开销太过昂贵了,React的更新性能会变得非常低效;
    1. 于是,React对这个算法进行了优化,将其 优化成O(n), 如何优化的呢?
    • 3.1. 同层节点之间相互比较,不会跨节点比较
    • 3.2. 不同类型的节点,产生不同的树结构
    • 3.3. 开发中,可以通过key来制定哪些节点在不同的渲染下保持稳定;
      • 不同节点直接生成新的DOM结构图:
      • 同层节点进行对比图:
      • 子元素有对应的key的话,会尽量来对比复用某些节点,之后插入一些新插入的节点;绑定key值插入元素对比图:

    1. 在前面遍历列表时,总是会提示一个警告,让我们加入一个key属性
    • 4.1. 方式一:在最后位置插入数据
      • 这种勤快,有无key意义并不大
    • 4.2. 方式二:在前面插入数据
      • 这种做法,在没有key的情况下,所有的li都需要进行修改;
    • 4.3. 当子元素(这里的li)拥有key时,React使用key来匹配原有树上的子元素以及最新树上的子元素;
      • 在下面这种场景下,key为c和d的元素仅仅进行位移,不需要进行任何的修改;
      • 将key为e的元素插入到前面的位置即可
      • 如下图:
    • 4.4. key的注意事项:
      • key应该是唯一的;
      • key不要使用随机数(随机数在下一次render时,会重新生成一个数字)
      • 使用index作为key, 对性能是没有优化的
2.3. render函数被调用
    1. 我们使用之前的一个嵌套案例
    • 在App中,增加一个计数器的代码
    • 当点击+1时,会重新调用App的render函数
    • 而当App的render函数被调用时,所有的子组件的render函数都会被重新调用;
    1. 那么,我们可以思考一下,在以后的开发中,我们只要是修改了App中的数据,
      所有组件都需要重新render,进行diff算法,性能必然是很低的
    • 事实上,很多的组件是没有必须要重新render;
    • 它们调用render应该有一个前提,就是依赖的数据(state、props)发生改变时,在调用自己的render方法
    1. 如何来控制render方法是否被调用呢?
    • 通过shouldComponentUpdate方法即可;
2.4. shouldComponentUpdate
    1. React给我们提供了一个生命周期方法shouldComponentUpdate(很多时候,我们简称为SCU),这个方法接受参数,并且需要有返回值:
    1. 该方法有两个参数:
    • 参数一: nextProps修改之后,最新的props属性
    • 参数二: nextState修改之后,最新的state属性
    1. 该方法返回值是一个boolean类型:
    • 返回值为true, 那么久需要调用render方法;
    • 返回值为false,那么就不需要调用render方法;
    • 默认返回的是true, 也就是只要state发生改变,就会调用render方法;
    1. 比如我们在App中增加一个message属性:
    • jsx中并没有依赖这个message, 那么它的改变不应该引起重新渲染;
    • 但是因为render监听到state的改变,就会重新render,所以最后render方法还是被重新调用了
2.5. PureComponent
    1. 如果所有的类,我们都需要手动来实现shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。
    • 我们来设想一下shouldComponentUpdate方法中的各种判断的目的是什么?
    • props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回的true或者false;
    1. 事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现嗯?
    • 将class继承自PureComponent
    1. 如果想要通过React机制进行性能优化导入PureComponent, 继承到Class组件上
    js 复制代码
      import React, { PureComponent } from 'react'
      export class App extends PureComponent {
        constructor () {
          super()
    
          this.state = {
            message: 'Hello World',
            counter: 0
          }
        }
    
        render() {
          // 初始化的时候,App组件和子组件的render函数都会执行
          console.log('App render')
          const { message, counter } = this.state
          return (
            <div>
              <h2>App Component: {message} - {counter}</h2>
              <button onClick={e => this.changeText()}>修改文本</button>
              <button onClick={e => this.changeCounter()}>counter+1</button>
              {/* <Home />
              <Recommend /> */}
              <Home message={message}/>
              <Recommend counter={counter}/>
              <Profile message={message} />
            </div>
          )
        }
      }
    1. PureComponent内部的操作就是自动的对state和props进行一个相关判断,是返回一个false还是一个true
    1. PureComponent本质是进行一个浅层的比较,本身内部的源码是做了一个浅层的比较,只比较第一层是不是同一个对象
    1. 示例代码如下:
    • App.jsx
    jsx 复制代码
      import React, { Component, PureComponent } from 'react'
      import Home from './Home'
      import Recommend from './Recommend' 
      import Profile from './Profile'
    
      // 如果想要通过React机制进行性能优化导入PureComponent, 继承到Class组件上
      // PureComponent内部的操作就是自动的对state和props进行一个相关判断,是返回一个false还是一个true
      // PureComponent本质是进行一个浅层的比较,本身内部的源码是做了一个浅层的比较,只比较第一层是不是同一个对象
      export class App extends PureComponent {
        constructor () {
          super()
    
          this.state = {
            message: 'Hello World',
            counter: 0
          }
        }
    
        // shouldComponentUpdate (nextProps, nextState) {
        //   // App中可以性能优化的点
        //   if(this.state.message !== nextState.message || this.state.counter !== nextState.counter) {
        //     return true
        //   }
        //   return false
        // }
    
        changeText () {
          // 当修改message的值,App的render函数会执行,Home和Recommend的render函数会不会执行呢?
          // Home和Recommend的render函数也会执行
          // this.setState({
          //   message: '你好啊, 李银河'
          // })
          // 如果设置的是和原来一样的值,依然是会触发render函数执行
          // 这样都执行性能不高,不高在两个地方
            // - App的render函数重新执行了  App -> render()
            // - 子组件: Home和Recommend的render函数重新执行了 Home/Recommend -> render()
          // 对于以上这种情况是没有必要执行render函数的 可以使用shouldComponentUpdate方法来优化性能
          this.setState({ 
            message: 'Hello World'
          })
        }
        changeCounter () {
          this.setState({
            counter: this.state.counter + 1
          })
        }
        render() {
          // 初始化的时候,App组件和子组件的render函数都会执行
          console.log('App render')
          const { message, counter } = this.state
          return (
            <div>
              <h2>App Component: {message} - {counter}</h2>
              <button onClick={e => this.changeText()}>修改文本</button>
              <button onClick={e => this.changeCounter()}>counter+1</button>
              {/* <Home />
              <Recommend /> */}
              <Home message={message}/>
              <Recommend counter={counter}/>
              <Profile message={message} />
            </div>
          )
        }
      }
    
      export default App
    • Home.jsx
    jsx 复制代码
      import React, { PureComponent } from 'react'
    
      export class Home extends PureComponent {
        constructor(props) {
          super(props)
    
          this.state = {
            
          }
        }
    
        // // 这个组件要不要更新
        // shouldComponentUpdate(nextProps, nextState) {
        //   // 自己对比state是否发生改变: this.state和nextState
        //   // shouldComponentUpdate的原理就是取原来的值和更新后的值进行对比,如果不一样更新,一样不更新render
        //   if(this.props.message !== nextProps.message) {
        //     return true
        //   }
        //   return false
        // }
        
        render() {
          console.log('Home render')
    
          return (
            <div>
              <h2>Home Component: {this.props.message}</h2>
            </div>
          )
        }
      }
    
      export default Home
    • Recommend.jsx
    jsx 复制代码
    import React, { PureComponent } from 'react'
    
    export class Recommend extends PureComponent {
      // shouldComponentUpdate(nextProps, nextState) {
      //   if(this.props.counter !== nextProps.counter) {
      //     return true
      //   }
      //   return false
      // }
      render() {
        console.log('Recommend render')
    
        return (
          <div>
            <h2>Recommend Component: {this.props.counter}</h2>
          </div>
        )
      }
    }
    
    export default Recommend
2.6. 高阶组件memo
    1. 目前是针对类组件可以使用PureComponent,那么函数式组件呢?
    • 事实上函数组件我们在props没有更改时,也是不希望其重新渲染DOM树结构;
    1. 需要使用一个高阶组件memo
    • 2.1. 将之前的封装好的函数式组件通过memo函数进行一层包裹;
    • 2.2. 最终的效果是,当数据发生改变时,函数式组件会重新执行,否则不会重新执行;
    1. 使用memo高阶函数对函数式组件进行性能优化
    1. 示例代码如下:
    jsx 复制代码
      import { memo } from "react";
    
      // memo高阶函数
      const Profile = memo(function (props) {
        console.log('Profile render')
        return (
          <div>
            <h2>Profile Component: {props.message}</h2>
          </div>
        )
      })
    
      export default Profile;
2.7. 数据不可变的力量
    1. 不可变的力量:不要直接去修改state里面的数据,而是把整个数据都修改掉,整个东西指向的内存全部修改掉,引用类型必须这样操作,值类型当设置一个新的值,本身这个东西就是全部修改掉了
    1. 直接修改原有的state,重新设置一遍,在PurComponent是不能引起重新渲染(re-render)
    • 关键代码如下:
    jsx 复制代码
    addNewBook () {
      this.state.books.push({
        name: 'Vue高级程序设计',
        price: 95,
        count: 1
      })
      this.setState({
        books: this.state.books
      })
    }
    1. 复制一份books,在新的books中修改,设置新的books, 本质上指向的是同一个对象, 新数组的目的是保证一定可以执行render函数
    • 内存地址如下图:

    • 关键代码如下:

      jsx 复制代码
      addNewBook () {
        const newBooks =[...this.state.books]
        newBooks.push({
          name: 'Vue高级程序设计',
          price: 95,
          count: 1
        })
        this.setState({
          books: newBooks
        })
      }
  • 4.完整代码如下:

    jsx 复制代码
      import React, { PureComponent } from 'react'
    
      export class App extends PureComponent {
        constructor() {
          super()
    
          this.state = {
            // React中要求state里面的数据都是不可变的
            // 如果想要改变,把整个对象改掉
            books: [
              { name: '你不知道js', price: 99, count:1 },
              { name: 'JS高级程序设计', price: 88, count:1 },
              { name: 'React高级程序设计', price: 78, count:2 },
              { name: 'Vue高级程序设计', price: 95, count:1 },
            ]
          }
        }
    
        addBookCount(index) {
          // 不要这样的写法
          // this.state.books[index].count++
          // 上面的修改和下面浅层复制一份修改,本质上指向的是同一个对象
          // 既然一样为什么要设置个新数组? 因为可以保证设置过去可以是一个新数组,新数组的目的是保证一定可以执行render函数
          const newBooks = [...this.state.books]
          newBooks[index].count++
          this.setState({
            books: newBooks
          })
        }
        //  shouldComponentUpdate (nextProps, nextState) {
        //   // 在PurComponent中,要不要修改,底层在实现shouldComponentUpdate就是把新旧props和state进行浅层比较, 发生改变返回true,否则返回false
        //   shallowEqual(nextProps, this.props)
        //   shallowEqual(nextState, this.state)
        //  }
    
        addNewBook () {
          // 不可变的力量
          // 1. 直接修改原有的state,重新设置一遍
          // 在PurComponent是不能引起重新渲染(re-render)
          // this.state.books.push({
          //   name: 'Vue高级程序设计',
          //   price: 95,
          //   count: 1
          // })
          // 会和原来的books进行比较,发现和原来的books是同一个对象,所以不会更新render
          // 只要放到state里面的数据不要直接修改它,它是不可变的
          // 如果要修改要重新设置新的对象好进行浅层比较,让React知道数据发生变化了
          // 这里的books是不可变的
          // this.setState({
          //   books: this.state.books
          // })
    
          // 2. 复制一份books,在新的books中修改,设置新的books
          const newBooks =[...this.state.books]
          newBooks.push({
            name: 'Vue高级程序设计',
            price: 95,
            count: 1
          })
          this.setState({
            books: newBooks
          })
        }
    
        render() {
          const { books } = this.state
          return (
            <div>
              <h2>书籍列表</h2>
              <ul>
                {
                  books.map((item, index) => {
                    return (
                      <li key={index}>
                        <span>name: {item.name} - price: {item.price} - count: {item.count} </span>
                        <button onClick={() => this.addBookCount(index)}>+1</button>
                      </li>
                    )
                  })
                }
              </ul>
              <button onClick={e => this.addNewBook()}>添加新书籍</button>
            </div>
          )
        }
      }
    
      export default App
    1. 源码部分:
      1. 源码部分判断一个组件是否要更新如下图:

      1. 源码里浅层对比逻辑:
      1. 底层做浅层比较的三个地方:
      • 2.1. 第一个做了一个标识:isPureReactComponent
      • 2.2. 第二个判断是否是纯函数,那么进行一个浅层比较
      • 2.3. 第三个在shallowEqual中进行比较
相关推荐
起风了___6 小时前
Flutter 全局音频播放单例实现(附完整源码)——基于 just_audio 的零依赖方案
前端·flutter
im_AMBER6 小时前
React 09
前端·javascript·笔记·学习·react.js·前端框架
TDengine (老段)6 小时前
益和热力性能优化实践:从 SQL Server 到 TDengine 时序数据库,写入快 20 秒、查询提速 5 倍
大数据·数据库·物联网·性能优化·时序数据库·tdengine·1024程序员节
用户4099322502126 小时前
快速入门Vue模板里的JS表达式有啥不能碰?计算属性为啥比方法更能打?
前端·ai编程·trae
非专业程序员6 小时前
HarfBuzz 实战:五大核心API 实例详解【附iOS/Swift实战示例】
前端·程序员
DreamMachine7 小时前
Flutter 开发的极简风格音乐播放器
前端·flutter
前端老宋Running7 小时前
前端防抖与节流一篇讲清楚
前端·面试
ejinxian7 小时前
Rust UI 框架GPUI 与 Electron 的对比
前端·javascript·electron
小马哥learn7 小时前
Vue3 + Electron + Node.js 桌面项目完整开发指南
前端·javascript·electron