16.React性能优化SCU
2. React性能优化SCU
PureComponent
memo
数据不可变力量
books
this.state.books.splice(2,1)
this.setState({books:this.state.books})
-
2.1. React更新机制
-
- 在前面已经了解过React的渲染流程:
编写jsx -> babel转换成React.createElement -> ReactElement -> 虚拟DOM(形成树结构) -> 真实DOM
-
- 那么React的更新流程呢?
-
-
2.2. React的更新流程
-
- React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树。
-
- React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI:
- 如何一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法复杂程度为O(n^3),其中n是树中元素的数量;
- [外链图片转存中…(img-E7dHwjHv-1761926407037)]https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf
- 如果在React中使用了该算法,那么展示1000个元素所需要执行的计算量将在十亿的量级范围;
- 这个开销太过昂贵了,React的更新性能会变得非常低效;
-
- 于是,React对这个算法进行了优化,将其 优化成O(n), 如何优化的呢?
- 3.1. 同层节点之间相互比较,不会跨节点比较
- 3.2. 不同类型的节点,产生不同的树结构
- 3.3. 开发中,可以通过key来制定哪些节点在不同的渲染下保持稳定;
- 不同节点直接生成新的DOM结构图:

- 同层节点进行对比图:

- 子元素有对应的key的话,会尽量来对比复用某些节点,之后插入一些新插入的节点;绑定key值插入元素对比图:


- 不同节点直接生成新的DOM结构图:
-
- 在前面遍历列表时,总是会提示一个警告,让我们加入一个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函数被调用
-
- 我们使用之前的一个嵌套案例
- 在App中,增加一个计数器的代码
- 当点击+1时,会重新调用App的render函数
- 而当App的render函数被调用时,所有的子组件的render函数都会被重新调用;

-
- 那么,我们可以思考一下,在以后的开发中,我们只要是修改了App中的数据,
所有组件都需要重新render,进行diff算法,性能必然是很低的
- 事实上,很多的组件是没有必须要重新render;
- 它们调用render应该有一个前提,就是依赖的数据(state、props)发生改变时,在调用自己的render方法
- 那么,我们可以思考一下,在以后的开发中,我们只要是修改了App中的数据,
-
- 如何来控制render方法是否被调用呢?
- 通过
shouldComponentUpdate方法即可;




-
-
2.4. shouldComponentUpdate
-
- React给我们提供了一个生命周期方法
shouldComponentUpdate(很多时候,我们简称为SCU),这个方法接受参数,并且需要有返回值:
- React给我们提供了一个生命周期方法
-
- 该方法有两个参数:
- 参数一: nextProps修改之后,最新的props属性
- 参数二: nextState修改之后,最新的state属性
-
- 该方法返回值是一个boolean类型:
- 返回值为true, 那么久需要调用render方法;
- 返回值为false,那么就不需要调用render方法;
- 默认返回的是true, 也就是只要state发生改变,就会调用render方法;
-
- 比如我们在App中增加一个message属性:
- jsx中并没有依赖这个message, 那么它的改变不应该引起重新渲染;
- 但是因为render监听到state的改变,就会重新render,所以最后render方法还是被重新调用了
-
-
2.5. PureComponent
-
- 如果所有的类,我们都需要手动来实现shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。
- 我们来设想一下shouldComponentUpdate方法中的各种判断的目的是什么?
- props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回的true或者false;
-
- 事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现嗯?
- 将class继承自PureComponent
-
- 如果想要通过React机制进行性能优化导入PureComponent, 继承到Class组件上
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.statereturn (<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>)}} -
- PureComponent内部的操作就是自动的对state和props进行一个相关判断,是返回一个false还是一个true
-
- PureComponent本质是
进行一个浅层的比较,本身内部的源码是做了一个浅层的比较,只比较第一层是不是同一个对象
- PureComponent本质是
-
- 示例代码如下:
- App.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.statereturn (<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
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
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
-
- 目前是针对类组件可以使用PureComponent,那么函数式组件呢?
- 事实上函数组件我们在props没有更改时,也是不希望其重新渲染DOM树结构;
-
- 需要使用一个高阶组件memo
- 2.1. 将之前的封装好的函数式组件通过memo函数进行一层包裹;
- 2.2. 最终的效果是,当数据发生改变时,函数式组件会重新执行,否则不会重新执行;
-
- 使用memo高阶函数对函数式组件进行性能优化
-
- 示例代码如下:
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. 数据不可变的力量
-
- 不可变的力量:不要直接去修改state里面的数据,而是把整个数据都修改掉,整个东西指向的内存全部修改掉,引用类型必须这样操作,值类型当设置一个新的值,本身这个东西就是全部修改掉了
-
- 直接修改原有的state,重新设置一遍,在PurComponent是不能引起重新渲染(re-render)
- 关键代码如下:
addNewBook () {this.state.books.push({name: 'Vue高级程序设计',price: 95,count: 1})this.setState({books: this.state.books}) } -
- 复制一份books,在新的books中修改,设置新的books, 本质上指向的是同一个对象, 新数组的目的是保证一定可以执行render函数
- 内存地址如下图:

- 关键代码如下:
addNewBook () {const newBooks =[...this.state.books]newBooks.push({name: 'Vue高级程序设计',price: 95,count: 1})this.setState({books: newBooks}) }
-
4.完整代码如下:
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中修改,设置新的booksconst newBooks =[...this.state.books]newBooks.push({name: 'Vue高级程序设计',price: 95,count: 1})this.setState({books: newBooks})}render() {const { books } = this.statereturn (<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 -
- 源码部分:
-
- 源码部分判断一个组件是否要更新如下图:


- 源码部分判断一个组件是否要更新如下图:
-
- 源码里浅层对比逻辑:

- 源码里浅层对比逻辑:
-
- 底层做浅层比较的三个地方:
- 2.1. 第一个做了一个标识:isPureReactComponent

- 2.2. 第二个判断是否是纯函数,那么进行一个浅层比较

- 2.3. 第三个在shallowEqual中进行比较

-

