React 学习之 PureComponent 与 React.memo

1,828 阅读4分钟

PureComponent

我们可以称之为 纯组件,用于避免不必要的渲染 (render 函数),从而提高效率

我们先看一个栗子

import React, { Component } from 'react'

/* App 引入渲染的组件 */
export default class TaskContainer extends Component {
    state = {
        tasks: []
    }
    componentDidMount() {
        const tasks = []
        for (let i = 0; i < 10; i++) {
            tasks.push({
                name: `Task ${i}`,
                isFinish: Math.random() > 0.5
            })
        }
        this.setState({ tasks })
    }
    addTask = newTask => {
        this.setState({
            tasks: [...this.state.tasks, newTask]
        })
    }
    render() {
        console.log('TaskContainer Render')
        return (
            <div>
                <AddTask onAdd={this.addTask}/>
                <TaskList tasks={this.state.tasks} />
            </div>
        )
    }
}
/* 添加一条新任务 */
class AddTask extends Component {
    state = {
        name: ''
    }
    handleChange = e => {
        this.setState({ name: e.target.value })
    }
    addTask = () => {
        /*
            使用运算符: ?.
            可省去: this.props.onAdd &&
        */
        this.props.onAdd?.({
            name: this.state.name,
            isFinish: false
        })
    }
    render() {
        console.log('AddTask Render')
        return (
            <div>
                <input type="text" value={this.state.name} onChange={this.handleChange} />
                <button onClick={this.addTask}>Add Task</button>
            </div>
        )
    }
}
/* 任务列表 */
class TaskList extends Component {
    render() {
        console.log('TaskList Render')
        const ts = this.props.tasks.map((task, i) => <Task key={i} {...task} />)
        return ( <ul> {ts} </ul> )
    }
}
/* 单个任务 */
class Task extends Component {
    render() {
        console.log('Task Render')
        const {name, isFinish} = this.props
        {/* 通过类名样式区分是否是完成状态 */}
        return (
            <li className={isFinish ? "finish" : ""}>{name}</li>
        )
    }
}
// const Task = props => {
//     console.log('Task Render')
//     const {name, isFinish} = props
//     return (
//        <li className={isFinish ? "finish" : ""}>{name}</li>
//     )
// }

初次渲染与 componentDidMount 获取任务列表后渲染的结果

初次渲染结果

从这张截图我们可以看到组件挂载完毕后,由于 TaskContainer 组件 state 中任务列表数据变化,所以导致两次 render 确实是正常的结果,但我们可以看到 AddTask 组件本身状态并没有任何的改变,却有一次 多余render

点击按钮添加一条新任务时的渲染结果

1626526433(1).jpg

从这张截图我们也可以看到,点击按钮时,AddTask 组件的状态其实也没有变动,但由于 TaskContainer 组件中任务列表新增一条任务,从而导致其下的所有组件都重新渲染了一遍,主要是前 10 条 Task 本身没有任何变动,却也依旧重新渲染了一遍

优化思路与优化方案

如果一个组件的 属性状态 均未发生变化,则我们可以认为该组件重新渲染是没有必要的

所以,在类组件中,我们就可以借助 shouldComponentUpdate(nextProps, nextState) 生命周期钩子函数进行简单的优化,先写一个浅比较帮助函数 objectEqual(origin, target)

// helper.js
export function objectEqual(origin, target) {
    for (let prop in origin) {
        // 存在属性值不同
        if (!Object.is(origin[prop], target[prop]) {
            return false
        }
    }
    // 所有属性值均相同(对象地址相同 & 基础数据类型值相同)
    return true
}

那么我们就可以在类组件中添加 shouldComponentUpdate 生命周期函数 (以 Task 组件为例):

import {objectEqual} from '../utils/helper'
class Task extends Component {
    shouldComponentUpdate(nextProps, nextState) {
        return !(objectEqual(this.props, nextProps) && objectEqual(this.state, nextState))
        // 或者好理解一点,这样写:
        // if (
        //   objectEqual(this.props, nextProps)
        //   && objectEqual(this.state, nextState)
        // ) {
        //     return false
        // }
        // return true
    }
    render() {
        console.log('Task Render')
        const {name, isFinish} = this.props
        return (
            <li className={isFinish ? "finish" : ""}>{name}</li>
        )
    }
}

再次 新增任务 时,你就会发现 Task Render 就只会打印 一次 了 (之前的 10 个任务组件就没有重新渲染了)


而这个方案其实并不需要我们自己去实现,因为 React 已经帮我们封装好了这么个优化方案的组件,那就是 PureComponent,只要我们写的类组件继承自它,它内部默认会在 shouldComponentUpdate 中对属性和状态进行浅比较优化。我们只需要将 extends Component 更改为 extends PureComponent 即可


注意点 —— ·PureComponent· 与 ·memo·

  1. PureComponent 进行的是 浅比较,所以,为了效率的提升,书写类组件时尽量使用 PureComponent,而且不要改动之前的状态,永远都要创建新的状态去覆盖之前的状态 (在 React 中运用一般运用 Immutable,即不可变对象保证每次都是产生新的对象)

    以上面新增任务处理函数为例,如下代码新增一条任务时,数组之中确实会新增一条任务,但并不会运行 render,因为 state 中的 tasks 还是指向之前的对象PureComponent 中经过对比发现,对象引用并未改变,则传给 TaskList 组件的 tasks 属性并未变化,其下的组件并不会重新渲染,页面上任务列表并不会更新

    import React, { PureComponent } from 'react'
    export default class TaskContainer extends PureComponent {
        state = {
            tasks: []
        }
        componentDidMount() {
            // ... get tasks
        }
        /* =========== attention =========== */
        addTask = newTask => {
            this.state.tasks.push(newTask)
            this.setState({
                tasks: this.state.tasks
            })
        }
        /* =========== attention =========== */
        render() {
            console.log('TaskContainer Render, tasks.length:', this.state.tasks.length)
            return (
                <div>
                    <AddTask onAdd={this.addTask}/>
                    <TaskList tasks={this.state.tasks} />
                </div>
            )
        }
    }
    
  2. 函数组件则使用 React.memo 函数去创建,如上面的 Task组件,函数纯组件写法:

    import React, {memo} from 'react'
    
    const Task = props => {
        console.log('Task Render')
        const {name, isFinish} = props
        return <li className={isFinish ? "finish" : ""}>{name}</li>
    }
    
    export default memo(Task) // 外面引用它,即和继承 PureComponent 的类组件是同样的效果
    
    /* ====================== 华丽的分割线 ====================== */
    
    // memo 函数的实现方案应该是:
    function memo(FuncComp) {
        return class Memo extends PureComponent {
            render() {
                return <FuncComp {...this.props} />
            }
        }
    }
    

以上,便是本篇对 PureComponent 组件的认识与 React 渲染方面的简单优化方案