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的更新性能会变得非常低效;
-
- 于是,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组件上
jsimport 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> ) } } -
- PureComponent内部的操作就是自动的对state和props进行一个相关判断,是返回一个false还是一个true
-
- PureComponent本质是
进行一个浅层的比较,本身内部的源码是做了一个浅层的比较,只比较第一层是不是同一个对象
- PureComponent本质是
-
- 示例代码如下:
- App.jsx
jsximport 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
jsximport 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
jsximport 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高阶函数对函数式组件进行性能优化
-
- 示例代码如下:
jsximport { 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)
- 关键代码如下:
jsxaddNewBook () { this.state.books.push({ name: 'Vue高级程序设计', price: 95, count: 1 }) this.setState({ books: this.state.books }) } -
- 复制一份books,在新的books中修改,设置新的books, 本质上指向的是同一个对象, 新数组的目的是保证一定可以执行render函数
-
内存地址如下图:

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


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

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

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

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