react学习笔记

本文详细介绍React性能优化策略,包括shouldComponentUpdate、PureComponent、memo、useEffect、useCallback、useMemo、lazy/Suspense、错误边界、Portals、PropTypes及immutable.js的使用,帮助开发者掌握提升React应用效率的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

React 优化组件

1. shouldComponentUpdate

使用 shouldComponentUpdate 生命周期函数(简称 SCU)可以优化 React 组件。SCU 可以让我们自己控制组件是否进行渲染。它返回一个布尔值,true 代表重新渲染,false 代表不渲染。默认 SCU 返回 ture。即:父组件更新会连带着子组件也更新,有时候重新渲染子组件并没有必要。使用 SCU 可以优化组件渲染。

shouldComponentUpdate(nextProps, nextState) {
  if (this.props.color !== nextProps.color) {
    return true;    // 不相等时返回 true,更新组件
  }
  if (this.state.count !== nextState.count) {
    return true;    // 相等时返回 false,不更新组件
  }
  return false;
}

React 提供了 PureComponent 类自动为我们做优化,使用时把组件继承这个类即可。

class Example extends PureComponent{
  // ...
}

PureComponent 使用“浅比较”的模式来检查 props 和 state 中所有的字段,以此来决定是否组件需要更新。“浅比较”不会比较对象内部的属性。例如 props 的数据如下:

props = {
  obj: {
    a: 1,
    b: 2
  },
  count: 1
}

比较时,不会比较 obj 对象内部的属性,只是比较 obj 地址有没有发生变化。每次更新都要生成一个新的对象才行,因此,SCU 必须配合“不可变值”一起使用(数据变化必须返回新的数据,而不是接着使用原来的数据,尤其是对于引用类型来说)。

浅比较就是两个变量引用值相等,使用 === 衡量。比如下面的都是浅比较:

var a = 2,b = 2;
console.log(a === b);   // true

var c = {a: 1};
var d = {a: 1};
// c 和 d 虽然对象中的内容相同,但是地址不同
console.log(c === d);   // false

而深比较是“原值相等”,深比较不使用运算符,而是需要实现一个深比较的函数。比如上面的代码中,对象 c 与对象 d 进行深比较时,因为 c 和 d对象中的属性都相等,因此为 true。 深比较会比较耗费性能。

function deepEqual(o1,o2){
    // ... 具体实现
}
console.log(deepEqual(c,d));    // true  

如果你想无论 props/state 的值有没有改变都要更新组件,那么就不要使用 PureComponent 或者 shouldComponentUpdate。因为使用的话,你的程序很可能会出现 bug。

还有一点需要注意,因为 PureComponent 是浅比较,如果你的 props/state 中有数组或者对象更新了其中的元素或者属性,PureComponent 并不会认为有更新。因此如果一个组件不是纯函数组件(组件中没有 props 和 state),就需要考虑使用 PureComponent 会不会影响组件渲染效果。

由于 PureComponent 是浅比较,当数据结构很复杂时,情况会变得麻烦。对于函数组件可以使用 memo 进行包裹,与 PureComponent 一样默认使用“浅比较”。

function MyCom(){
  // ...
}
export default React.memo(MyCom);

也可以给 memo 函数传入第二个参数,这个函数与 SCU 函数返回值相反。


function areEqual(prevProps, nextProps){
  // props 不相等时返回 false,表示更新组件
  // props 相等时返回 true,表示不更新组件
}

React.memo(MyCom, areEqual);

下图是 React 的生命周期函数。
react生命周期
从图中可以看出,SCU 处在 render 之前,它可以拦截组件的 propsstate

2. useEffect

useEffect React Hooks 中的一个钩子函数。effect hooks可以让你在函数组件中执行副作用操作。

useEffect 函数很强大。使用这个函数可以模拟 React 当中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个生命周期函数。因此合理地使用 Effect 至关重要。

componentDidMountcomponentWillUnmount 在整个组件的生命周期中只会执行一次,而 componentDidUpdate 表示组件更新完毕,因此当组件有更新后,该函数就会被执行。

通常在 componentDidMount 中会写一些副作用,比如开始的 Ajax 请求、记录日志、手动的变更 DOM 等操作。现在使用 Effect 也可以做到。

useEffect 函数接收两个参数,第一个参数是一个回调函数,在里面写入的是一些副作用;第二个参数是个可选参数,Effect 之所以能够模拟生命周期函数就是依靠第二个参数。

第二个参数是一个数组,当不是空数组时,数组里的内容应该是一个个的 props 或者 state,表示当数组中的 props/state 发生变化时,useEffect 的第一个参数(回调函数)就会再次执行(这有些像 PureComponent 组件)。如果不传第二个参数,它在第一次渲染之后和每次更新之后都会执行。而如果传入的是一个空数组,Effect 函数只运行一次(组件挂载时:componentDidMount) 。

除此之外,useEffect 函数还可以返回一个函数,React 会在组件卸载的时候执行这个函数。当 Effect 的第二个参数是空数组时,这相当于模拟了 componentWillUnmount 函数的作用。

下面的例子,当点击按钮时,count 就会变化,切换浏览器标签页时文档 title 会发生改变。

function App() {
    let [count, setCount] = useState(0);
    let [normalTitle,setNormalTitle] = useState("");

    useEffect(() => {
        document.title = `You clicked ${count} times`;
        setNormalTitle(document.title);
    },[count]);

    useEffect(() => {
        // 当浏览器切换到别的的标签页时会触发该事件
        document.onvisibilitychange = function(){
            console.log(this.visibilityState);
            if(document.visibilityState === "hidden"){
                document.title = "不要走呀~~";
            }else{
                document.title = normalTitle;
            }
        }
    },[normalTitle]);

    function handleClick(){
        setCount(count + 1);
    }

    return (
        <button onClick={handleClick}>Click</button>
    );
}

3. useCallback 和 useMemo

在使用 React Hook 时,我们常常会用到 useCallbackuseMemo,这两个 API 都可以传入一个 deps 数组,与 useEffect 中的 deps 相比三者有什么不同之处呢?

useCallbackuseMemo 的行为相似,useCallback 会返回一个 memoized 的回调函数。而 useMemo 会返回一个 memozied 的值。

memoized 回调函数会在 deps 中的某个依赖项发生变化时才被调用;memoized 值会在 deps 中的某个依赖项发生变化时才重新计算。

如果 useCallbackuseMemo 不传第二个参数,每次渲染都会执行函数或重新计算新的值,而如果传入的是空数组,则只初始渲染时执行一次(空数组表明没有依赖项)。使用 useMemo 有助于避免在每次渲染时都进行高开销的计算。

useEffect 中的 deps 与两种不同。不传入 deps 参数时,每次渲染 useEffect 回调都会被执行。而 deps 如果是空数组时,只会在 componentDidMount 阶段执行一次,这可以处理一些副作用,比如发起网络请求,设置定时器等。如果 useEffect 回调函数内返回了一个函数,这个返回的函数会在 componentWillUnmount 阶段执行。如果 deps 中传入了依赖项,当某个依赖项发生变化时 useEffect 回调会被执行。

useEffect 相似的还有一个 useLayoutEffect API,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用(异步执行),不应在这个函数中执行阻塞浏览器更新屏幕的操作;useLayoutEffect 会在所有的 DOM 变更之后同步调用 effect(在浏览器执行绘制之前),使用它来读取 DOM 布局并同步触发重渲染,它会阻塞浏览器的绘制。useLayoutEffect 要比 useEffect 执行时机早。尽量使用 useEffect

4. lazy/Suspense

React.lazy 函数能让你像渲染常规组件一样处理动态引入的组件。而 Suspense 是一个组件,这两个东西一般是配合使用的。

在 webpack 中如果做文件打包,打包出来的文件可能会很大。而打包好的文件中可能有一些代码并不需要每次加载页面时就请求它(或说使用到它),比如当用户点击按钮时才会运行某一些代码。这时候就可以使用异步的方式再去获取资源。

// 异步的导入 print.js 文件
button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
    // print 函数 存在于 module.default 中
    var print = module.default;
    // 执行异步加载到的 print 函数
    print();
});

React 的 lazy 函数与之类似。在组件首次被渲染时,就会自动导入这个被懒加载的组件。

const LazyComponent = React.lazy(() => import('./LazyComponent'));

lazy 必须与 Suspense 组件一起使用。例如下面的代码,当 count 大于 6 时,就会动态插入 Text 组件:

import React,{lazy,Suspense,useCallback,useState} from "react";
// 懒加载
const Text = lazy(() => import("./Text.jsx"));

function App(){
    let [count,setCount] = useState(0);

    var handleClick = useCallback(function(){
        setCount(count + 1);
    },[count]);

    return (
        <>
            <h2>The number is: {count}</h2>
            {
                count > 6 ? 
                    <Suspense fallback={<div>Loading...</div>}>
                        <Text />
                    </Suspense>
                : ""
            }
            <button onClick={handleClick}>Click</button>
        </>
    );
}

// Text.jsx
import React,{memo} from "react";

function Text(props){
    return (
        <p>
            Lorem, ipsum dolor sit amet consectetur adipisicing elit. Cum aspernatur a asperiores consequatur qui explicabo, excepturi, eos ea, sequi quis perferendis. Accusamus velit eos accusantium facilis dolor, quas cupiditate. Esse.
        <p>
    );
}

import default memo(Text);

Suspense 组件必须有一个 fallback 属性。fallback 的值应是一个组件,它表示懒加载的组件在没有加载到页面之前应显示的效果,通常是一个 Loading

5. 错误边界

错误边界是一种React组件,这种组件可以捕获并打印发生在其 子组件树任何位置的JavaScript错误 ,并且,它会渲染出备用UI,而不是渲染那些崩溃了的子组件树。渲染期间,生命周期方法和整个组件树的构造函数中捕获错误。

需要注意的是,错误边界无法捕获以下场景产生的错误:

  • 事件处理
  • 初步代码(例如 setTimeoutrequestAnimationFrame 等函数)
  • 服务端渲染
  • 组件自身引起的错误(而非它的子组件)

可以这样实现一个错误边界组件:

class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }

    static getDerivedStateFromError(error) {
        // 更新 state 使下一次渲染能够显示降级后的 UI
        return { hasError: true };
    }

    componentDidCatch(error, errorInfo) {
        // 你同样可以将错误日志上报给服务器
        logErrorToMyService(error, errorInfo);
    }

    render() {
        if (this.state.hasError) {
            // 你可以自定义降级后的 UI 并渲染
            return this.props.fallback;
        }

        return this.props.children; 
    }
}

使用时,将这个组件包含在常规组件的外面:

// 发生错误时,fallback 就会被渲染出来
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
    <MyWidget />
</ErrorBoundary>

使用 lazy/Suspense 时,异步加载的组件可能没有加载成功,这时候也可以使用 ErrorBoundary 进行包裹:

import ErrorBoundary from './ErrorBoundary';
const OtherComponent = React.lazy(() => import('./OtherComponent'));

const MyComponent = () => (
    <div>
        <ErrorBoundary fallback={<p>Something went wrong.</p>}>
            <Suspense fallback={<div>Loading...</div>}>
                <OtherComponent />
            </Suspense>
        </ErrorBoundary>
    </div>
);

6. Portals

Portals 是 React16 新出的一个功能,被称为“插槽”。它可以将子节点渲染到存在于父组件以外的 DOM 节点上。

比如,一个组件本来在 <App /> 组件中,但是通过 Portal 可以将这个组件插入到页面的任意位置。

通过 PortalDialog 组件插入到 body 标签下。

import React, {useState} from 'react';
import Dialog from "./components/Dialog.jsx";
import "./App.sass";

function App(){
    let [isShow,setIsShow] = useState(false);
    function enterHandleClick(){
        setIsShow(true);
    }
    function hiddenBtn(){
        setIsShow(false);
    }
    return(
        <div className="wrapper">
            <div className="btn-wrapper">
                <button onClick={enterHandleClick}>确认提示框</button>
            </div>
            {/* 把提示框写在了这里 */}
            <div className="prompt-box">
                {
                    isShow ? 
                    <Dialog>
                        <div className="dialog-wrapper">
                            <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Porro quisquam deleniti qui quam deserunt. Voluptatum doloribus fugiat consectetur harum, aliquam eius hic, amet aperiam cum ipsum, explicabo quos. Mollitia, error.</p>
                            <div>
                                <button onClick={hiddenBtn} className="enter">确认</button>
                                <button onClick={hiddenBtn} className="cancel">取消</button>
                            </div>
                        </div>
                    </Dialog>
                    : ""
                }
            </div>
        </div>
    );
}
export default App;

下面是使用了 PortalDialog 组件:

import React,{useEffect} from "react";
import ReactDOM from "react-dom";

function Dialog(props){

    var el = document.createElement("div");

    // componentDidMount 时将 el 插入到 body 中
    useEffect(() => {   
        document.body.appendChild(el);        
        return () => {
            // 页面卸载时,清除 el 元素
            document.body.removeChild(el);
        }
    },[]);

    return ReactDOM.createPortal(
        // 插槽 jsx
        props.children,
        // 传送到另一端的元素节点
        el
    );
}
export default Dialog;

使用 React.createPortal 可以实现 Portal 插槽。这样,当点击 确认提示框 时,Dialog 组件实际是在 body 下,而不是在 App 组件下,因此编写 CSS 时应注意。

Portal 的用法和作用可以参看这篇文章:传送门:React Portal

7. PropTypes

PropTypes 可以给组件的 props 进行类型检查。PropTypes 需要另行下载:

npm install prop-types 

用法:

import PropTypes from "prop-types";

function App(props){
    return <h1>{props.name}</h1>
}

App.propTypes = {
    // name 应该是一个字符串类型的值
    name: PropTypes.string
};

PropTypes 的用法与类型可以参考 React 官网上的文档:PropTypes 文档说明

当然,除了 PropTypes 之外,也可以使用 TypeScript 来编写 React,typescript 相当于自带了 props 类型检测功能。

8. Immutable.js

immutable.js 是一个 JavaScript 库。使用时需要下载:

yarn add immutable

通过上面的 PureComponentmemo 我们已经知道,当 props/state 的数据类型是复杂类型时(比如数组或者对象),PureComonent/memo 可能就会出现bug。比如下面的代码,数组里的元素变了(数组倒序、正序),但是数组地址没变,而组件也并不会更新。

class App extends React.PureComponent{
    state = {
        arr: [2,4,6,1,3,5]
    };

    handleClick(){
        // 对数组进行排序
        state.arr.sort((a,b) => a - b);
        // 更新 state
        this.setState(state => ({
            arr: state.arr
        }));
    }

    render(){
        return (
            <>
                <button onClick={() => this.handleClick()}>Click</button>
                <ul>
                    {
                        this.state.arr.map(item => <li key={item}>The number is {item}</li>)
                    }
                </ul>
            </>
        );
    }
}

当点击按钮后,发现页面并没有发生变化,这是因为 sort 函数是对原数组进行排序,返回值并不是一个新的数组,而 PureComponent/memo浅比较,因此行不通。

在 React 中不要直接去使用数组的以下的几个方法,因为使用它们更新 props/state 很可能会出现 bug,因为它们都是修改原数组。

  • sort 给数组排序;
  • reverse 颠倒数组;
  • splice 从数组中添加/删除项目;
  • push 向数组尾部插入新的元素;
  • pop 数组尾部删除元素;
  • unshift 向数组的开头添加一个或更多元素,并返回新的长度;
  • shift 删除并返回数组的第一个元素;

如果要使用,可以结合 ES6 中的扩展运算,重新生成一个数组:

handleClick(){
    this.setState(state => {
        this.state.arr.sort((a,b) => b - a);
        return {
            // 使用数组扩展运算符
            arr: [...state.arr]
        }
    });
}

也可以使用对象的扩展运算符,或者使用 Object.assign 方法。比如下面的例子,当点击按钮后,salary 的数值就会改变,这是因为使用了 ES6 中的对象扩展。

class App extends React.PureComponent {
    state = {
        person: {
            name: "Jack",
            age: "18",
            number: 12345678910,
            salary: 3
        }
    };

    handleClick() {

        this.setState(state => {
            state.person.salary += 1;
            return {
                // 使用对象扩展运算
                person: {...state.person}
            }
        });
    }

    render() {
        return (
            <>
                <button onClick={() => this.handleClick()}>Click</button>
                <ul>
                    {
                        Object.keys(this.state.person).map(item => {
                            return <li key={item}>
                                <span>{item}: </span>
                                <span>{item === "salary" ? this.state.person[item] + "K" : this.state.person[item]}</span>
                            </li>
                        })
                    }
                </ul>
            </>
        );
    }
}

扩展运算也可以用 Object.assign 方法进行代替。

handleClick() {
    this.setState(state => {
        state.person.salary += 1;
        return {
            // 使用 Object.assign 方法
            person: Object.assign({},state.person)
        }
    });
}

无论是使用扩展运算符,还是使用 Object.assign 函数,它们只能进行一维的浅克隆。也就是说,面对二维数组、对象嵌套、数组与对象的嵌套时,这些方法,只能克隆外层,里面的复杂类型还是引用关系。这时候就要考虑如何实现深层次克隆比较。而 immediate.js 就是做这个工作的。

immutable 这个单词表示“不可改变的”。也就是说,数据一旦被 immutable.js 创建后,通过原生方式改变数据是不可以的,只有使用 immutable 内部提供的方法去进行数据变更。

import Immutable from "immutable";
// 可以查看到 immutable 内部提供的函数
console.log(Immutable);

使用 fromJS 方法可以将纯 JS 对象和数组深层转换为不可变映射和列表。 immutable 提供了 setget 方法,set 方法可以设置新的值,get 方法通过 key 的方式获取 valueset 方法设置新的值后,会返回一个全新的 immutable data。例如下面的 js 对象,使用 fromJS 包装,然后使用 get 方法可以获取对象的属性值,然后使用 set 方法改变原来的值并返回新的 对象

import {fromJS} from "immutable";

// 使用 fromJS 包装
var person = fromJS({
    name: "Jack",
    age: 18,
    salary: 3
});

// 改变 person 中的属性值通过 set
var salary = person.get("salary");
// newPerson 的 salary 值就会变成 4
// 而 person 中的 salary 值还是 3
var newPerson = person.set("salary",salary + 1);

immutable 实例中还有一个 toJS 方法,可以将被 immutable 化的原生 js 数据解构再转回来。比如上面的 newPerson 使用 toJS 后可以又变回原生 js 对象:

import {fromJS} from "immutable";
// ...
console.log(newPerson.toJS());
// {name: "Jack",age: 18,salary: 4}

immutable 实现了几乎所有的原生 js 支持的数据结构,但是这些数据结构的值都是不可变的,只有通过 set 方法才能获取更新后的数据结构。

immutable 还提供了 setIngetIn 方法,对象嵌套式的复杂数据结构,可以使用这两个方法很方便地获取到深层的 key 值。

const {fromJS} = require("immutable");

var obj = fromJS({
    a: 123,
    b: {
        name: "Jack",
        age: 28,

        child: {
            name: "Joy",
            age: 6
        },
    },
    c: [4,5,6,7,8],
    d: "Hello!"
});

// 更改属性 a 得值
var obj_1 = obj.set("a",456);
// 更改属性 b 里面的 child 属性里的 age 属性值
var obj_2 = obj.setIn(["b","child","age"],7);
// 获取到 b.child.name 属性
var childName = obj.getIn(["b","child","name"]);
// 将数组 c 中的下标是 1 的项(数组第二项)值改为 50
var obj_3 = obj.setIn(["c",1],50);

console.log(
    "obj_1: ",obj_1.toJS(),"\n",
    "obj_2: ",obj_2.toJS(),"\n",
    "childName: ",childName,"\n",
    "obj_3: ",obj_3.toJS(),"\n"
);

immutable.js 的使用可以查看这篇文档,因为 immutable 库挺大的,API 也比较多。

immutable 常用 API 简介

相比于深度克隆,Immutable.js 采用了持久化数据结构和结构共享,保证每一个对象都是不可变的,任何添加、修改、删除等操作都会生成一个新的对象,且通过结构共享等方式大幅提高性能。实现原理可以参考这篇博文:

深入探究immutable.js 的实现机制

当熟练使用 immutable 时就差不多能解决 react 组件不更新的问题了。

immutable 通常与 Redux 一起使用,这是因为 Redux 要求 reducer 中的 state 值是只读的,每次返回新的值时,我们都要克隆一份,然后做修改,最后返回(通常的做法可能就是使用扩展运算甚至是 JSON.stringifyJSON.parse)。

除了 immutable + redux 外,也可以使用 mobx 库进行状态管理。mobx 库使用起来也很方便,只是需要了解 JavaScript 的装饰器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值