React中的高阶组件(二)

155 阅读12分钟

接着介绍React中的高阶组件,上篇文章React中的高阶组件(一)

高阶组件(Higher-Order Component, HOC) 是React中为了复用组件的函数。一个 HOC 是一个函数,接收一个组件并返回一个增强版的组件。通过 HOC,开发者可以将通用逻辑提取到可复用的组件包装器中,避免代码重复。

5. withSuspense(React.lazy)

  • 功能:React 中使用 React.lazySuspense 实现组件的懒加载(Lazy Loading)。懒加载是一种优化页面性能的方法,通过延迟加载某些组件直到它们被真正需要时才进行加载,从而减少初始加载时间。
import React, { Suspense } from "react";

//**`React.lazy`** 是 React 提供的懒加载功能,用于动态导入组件。它接收一个函数作为参数,
//这个函数使用 `import()` 动态加载组件文件。这个函数的返回值是一个 Promise,`React.lazy` 
//会将这个 Promise 转化为可以使用的 React 组件。

//`React.lazy(() => import("./LazyComponent"))` 表示 `LazyComponent` 组件不会在应用启动时立即加载,
//而是当 `LazyComponent` 第一次被渲染时,才会触发动态导入,从而减少初始加载时的 JavaScript 包体积。
const LazyComponent = React.lazy(() => import("./LazyComponent"));

const MyApp = () => (
    //**`Suspense`** 是 React 中用于包裹懒加载组件的组件,它会在懒加载的组件加载完成之前显示
    //一个备用的 UI(即 `fallback`)。
    //`Suspense` 的 `fallback` 属性用于指定在等待懒加载组件加载时展示的内容。它可以是任何有效的 JSX 元素,
    //例如:加载动画、加载文本等。在上面的代码中,当 `LazyComponent` 还未加载完成时,会显示 `<div>Loading...</div>` 。
    <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
    </Suspense>
);

懒加载的优势

懒加载可以帮助提升应用的性能,尤其在以下场景中:

  • 首屏优化:如果某些组件不会立刻出现在首屏内容中(例如路由下的子页面组件),可以通过懒加载将它们延后加载,这样可以显著减小首屏加载时的 JavaScript 文件大小,加快页面的初始渲染速度。
  • 按需加载:对于一些不常用的组件(例如图片查看器、视频播放器等),可以通过懒加载让这些组件在真正需要时再加载,避免加载无用的资源。

代码流程解释

  1. 初始化应用:当应用开始运行时,LazyComponent 组件并不会立即被加载。React.lazy 会告诉 React 在需要的时候才加载这个组件。
  2. 首次渲染:当 MyApp 组件渲染时,Suspense 组件包裹了 LazyComponent。因为 LazyComponent 还没有加载完成,所以 Suspensefallback 内容会先显示出来。在这个例子中,<div>Loading...</div> 将首先渲染。
  3. 懒加载组件加载完成:当 LazyComponent 被成功加载后,React 会自动替换掉 fallback 占位符,并渲染真正的 LazyComponent 组件。

React.lazy 和 Suspense 的适用场景

React.lazySuspense 主要适用于大型应用中,以下是一些常见的使用场景:

  • 路由组件的懒加载:在使用 React 路由时,只有当某个路径被访问时才去加载该路径对应的组件。
  • 第三方库或模块的按需加载:一些大型的第三方库(例如图表库、富文本编辑器等),可以通过懒加载来避免初始加载时加载所有的库,只有当用户实际使用到这些功能时再去加载对应的模块。

例如,以下是一个常见的路由懒加载的场景:

import React, { Suspense } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";

const Home = React.lazy(() => import("./Home"));
const About = React.lazy(() => import("./About"));

const App = () => (
    <Router>
        <Suspense fallback={<div>Loading...</div>}>
            <Switch>
                <Route exact path="/" component={Home} />
                <Route path="/about" component={About} />
            </Switch>
        </Suspense>
    </Router>
);

在这个例子中,HomeAbout 组件只有在路由被匹配时才会被加载,这样可以显著减少应用的初始加载时间。

注意事项

  • 错误处理:默认情况下,React.lazy 的加载是异步的,可能会因为网络问题等原因加载失败。为了处理这种情况,可以配合 React.ErrorBoundary 进行错误处理,捕获加载错误并给用户提供反馈。
class ErrorBoundary extends React.Component {
    state = { hasError: false };

    static getDerivedStateFromError(error) {
        return { hasError: true };
    }

    render() {
        if (this.state.hasError) {
            return <div>Something went wrong...</div>;
        }
        return this.props.children;
    }
}

const App = () => (
    <ErrorBoundary>
        <Suspense fallback={<div>Loading...</div>}>
            <LazyComponent />
        </Suspense>
    </ErrorBoundary>
);

兼容性React.lazySuspense 目前只支持组件的懒加载,不支持非组件的懒加载(如函数或模块)。如果需要加载其他类型的模块,建议使用 Webpack 的动态 import()

总结

  • React.lazy:用于懒加载组件,它通过动态导入的方式按需加载组件,从而提升页面性能。
  • Suspense:用于处理懒加载组件的占位符渲染,当组件加载时展示一个 fallback 内容。
  • 使用场景:懒加载非常适合大型应用,尤其是当一些组件只在特定条件下使用时,可以通过懒加载优化初始加载时间,提升用户体验。

6. withLoading(加载状态处理)

  • 功能React.lazySuspense 目前只支持组件的懒加载,非组件的懒加载(如函数或模块)可以使用withLoading
  • 示例
const withLoading = (WrappedComponent) => {
    return class extends React.Component {
        state = { loading: true };

        componentDidMount() {
            fetchData().then(() => this.setState({ loading: false }));
        }

        render() {
            if (this.state.loading) {
                return <div>Loading...</div>;
            }
            return <WrappedComponent {...this.props} />;
        }
    };
};

export default withLoading;

1. withLoading 高阶组件

  • 参数 WrappedComponent:这是被包装的组件,withLoading 会为这个组件添加加载状态功能。
  • 返回一个新的类组件:高阶组件通常会返回一个类或函数组件,目的是在返回的组件中注入额外的功能。在这个例子中,返回了一个扩展自 React.Component 的类组件。

2. state = { loading: true }

  • 该组件有一个状态 loading,初始值为 true,表示组件在刚加载时处于加载中状态。
  • loading 状态将控制组件是否显示加载提示或实际内容。

3. componentDidMount()

  • 数据加载逻辑:在组件挂载时会调用 fetchData() 函数(这个函数需要根据实际情况定义),通常用于获取异步数据。获取完数据后,通过 this.setState({ loading: false }) 来更新 loading 状态为 false,表示加载完成。
  • 加载完成后,组件会重新渲染,并显示真正的 WrappedComponent

4. render()

  • 在渲染函数中,根据 loading 状态决定渲染什么内容。

    • 如果 loadingtrue,则渲染 <div>Loading...</div>,即加载提示。
    • 如果 loadingfalse,则渲染 WrappedComponent,并将传递给 withLoadingprops 全部传递给 WrappedComponent

5. export default withLoading

  • 这行代码将 withLoading 高阶组件导出,供其他模块使用。

使用方法

假设你有一个简单的组件 MyComponent,你可以通过 withLoading 高阶组件为它添加加载状态:

import React from 'react';
import withLoading from './withLoading';

const MyComponent = () => {
    return <div>My Component Content</div>;
};

export default withLoading(MyComponent);

在这个例子中,MyComponent 会在加载时显示 "Loading...",当数据加载完成后,才会显示 MyComponent 的内容。

延展fetchData()

为了让这个高阶组件更实用,fetchData() 通常会作为参数传入高阶组件,这样可以根据不同的需求动态传入不同的数据获取逻辑。以下是一个示例,包括如何使用 withLoading 包装组件并传入 fetchData 函数:

import React from 'react';

// 模拟的 fetchData 函数,返回一个 Promise,模拟异步数据获取
const fetchData = () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data loaded');
        }, 2000); // 模拟2秒的延迟
    });
};

// 高阶组件 withLoading
const withLoading = (WrappedComponent, fetchData) => {
    return class extends React.Component {
        state = { loading: true };

        componentDidMount() {
            fetchData().then(() => this.setState({ loading: false }));
        }

        render() {
            if (this.state.loading) {
                return <div>Loading...</div>;
            }
            return <WrappedComponent {...this.props} />;
        }
    };
};

// 被包装的组件 MyComponent
const MyComponent = (props) => {
    return <div>Component content goes here</div>;
};

// 使用 withLoading 包装 MyComponent 并传入 fetchData
const MyComponentWithLoading = withLoading(MyComponent, fetchData);

// 主应用组件
const App = () => {
    return (
        <div>
            <h1>My Application</h1>
            <MyComponentWithLoading />
        </div>
    );
};

export default App;

代码详解

  1. fetchData 函数:这是一个模拟的异步函数,返回一个 Promise,并在2秒后 resolve 模拟的数据。这个函数将作为参数传递给 withLoading 高阶组件。

  2. withLoading 高阶组件

    • 接受 WrappedComponentfetchData 作为参数。
    • componentDidMount 生命周期方法中调用 fetchData(),并在数据加载完成后将 loading 状态设置为 false
    • loadingtrue 时显示 "Loading...",当加载完成后显示被包装的组件。
  3. MyComponent:这是一个简单的展示组件。通过 withLoading 包装后,加载完成后会显示它的内容。

  4. MyComponentWithLoading:这是将 MyComponentfetchData 组合后的新组件。它会先显示加载状态,然后在数据加载完成后显示 MyComponent 的内容。

  5. App 组件:主应用组件,渲染 MyComponentWithLoading

执行流程

  1. 应用启动时,MyComponentWithLoading 通过 withLoading 高阶组件包装。
  2. withLoading 调用 fetchData,模拟数据加载,并显示加载中的提示。
  3. fetchData 在2秒后 resolve 数据时,loading 状态更新为 false
  4. 最终,加载完成后,MyComponent 的内容会替代加载中的提示被渲染出来。

运行后效果

  • 应用会先显示 Loading...,等待 2 秒钟后,页面会显示 Component content goes here

7. withTranslation(i18n 国际化)

withTranslation 是来自 react-i18next 的高阶组件,用于在 React 应用中实现国际化。通过它,可以将翻译函数 t 传递给组件,以便轻松地使用多语言支持。

使用示例:

import React from 'react';
import { withTranslation } from 'react-i18next';

const MyComponent = ({ t }) => {
    return <div>{t('welcome_message')}</div>;
};

export default withTranslation()(MyComponent);

解释

  • 这是一段函数式组件代码,使用了 withTranslation 高阶组件。
  • withTranslation()t 函数注入到 MyComponent 组件的 props 中,t('welcome_message') 表示从国际化资源中查找 welcome_message 对应的翻译内容并显示。
  • t('welcome_message') 会根据用户选择的语言来显示不同的文本。例如,如果用户选择了中文,它可能显示 欢迎消息

工作流程

  1. 通过 withTranslationi18n 的翻译函数 t 注入 MyComponent
  2. 在 JSX 代码中调用 t('welcome_message') 以获取翻译。
  3. 组件会根据当前语言的设置,显示相应的翻译文本。

代码二(使用类组件):

import { withTranslation } from 'react-i18next';

class NavbarComponent extends React.Component {
    render() {
        const { t } = this.props;
        return <div>{t('navbar.home')}</div>;
    }
}

export default withTranslation()(NavbarComponent);

解释

  • 这是使用类组件的代码,功能与第一段代码相似。
  • 同样通过 withTranslation() 高阶组件,将 t 函数注入到 NavbarComponentprops 中,t('navbar.home') 表示从国际化资源中查找 navbar.home 对应的翻译内容。
  • 与第一段代码不同的是,这里使用了类组件,而不是函数式组件。

工作流程

  1. 通过 withTranslationt 函数注入到类组件 NavbarComponent 中。
  2. render 方法中调用 t('navbar.home') 来获取翻译文本。
  3. 根据用户选择的语言,显示 navbar.home 的相应翻译内容。

总结

  • withTranslation() 是 React 中常用的高阶组件,主要作用是将 i18next 的翻译功能注入到组件中。
  • 无论是函数式组件还是类组件,withTranslation 都可以无缝集成国际化功能,开发者可以根据项目需求选择使用哪种组件风格。

8. withSubscription

withSubscription 是一种常见的高阶组件模式,主要用于订阅数据源并将数据传递给被包装的组件。它可以从事件、WebSocket 或数据流中订阅数据,然后将这些数据注入到组件中。

这段代码定义了一个名为 withSubscription 的高阶组件(Higher-Order Component, HOC),其主要作用是为一个包装的组件 WrappedComponent 提供订阅机制,动态更新数据,并且将数据传递给被包装的组件。以下是对代码的详细介绍和工作原理的解析:

//也可以写成const withSubscription_myname ,进行自定义名称
const withSubscription = (WrappedComponent, selectData) => {
    return class extends React.Component {
        constructor(props) {
            
            //`super()` 是用来调用父类的构造函数。
            //在 ES6 类继承中,如果一个类继承了另一个类(例如 `class MyComponent extends React.Component`),
            //那么在派生类的构造函数中必须首先调用 `super()`,才能访问 `this` 关键词。
            super(props);
            this.state = {
                data: selectData(DataSource, props), // 初始化状态,使用 selectData 函数从 DataSource 中获取数据
            };
        }

        componentDidMount() {
            // 组件挂载后,订阅数据源的变化
            DataSource.addChangeListener(this.handleChange);
        }

        componentWillUnmount() {
            // 组件卸载前,取消订阅
            DataSource.removeChangeListener(this.handleChange);
        }

        handleChange = () => {
            // 当数据源发生变化时,更新组件状态
            this.setState({
                data: selectData(DataSource, this.props),
            });
        };

        render() {
            // 将获取到的数据作为 props 传递给被包装的组件 WrappedComponent
            return <WrappedComponent data={this.state.data} {...this.props} />;
        }
    };
};

代码功能

  1. withSubscription 高阶组件:这是一个 HOC,它接受两个参数:

    • WrappedComponent:需要被增强的组件,即接收订阅数据的目标组件。
    • selectData:这是一个函数,负责从 DataSource 中选择需要的数据,并根据 props 提供的上下文对数据进行选择。
  2. 构造函数(constructor

    • 在组件的构造函数中,调用 selectData(DataSource, props) 来初始化数据,并将其存储在组件的 state 中。
  3. componentDidMount 生命周期方法

    • 当组件挂载到 DOM 后,调用 DataSource.addChangeListener(this.handleChange) 为数据源添加一个变化监听器。这样,数据源每次发生变化时,都会调用 handleChange 方法。
  4. componentWillUnmount 生命周期方法

    • 当组件即将从 DOM 卸载时,调用 DataSource.removeChangeListener(this.handleChange) 来移除监听器,防止内存泄漏。
  5. handleChange 方法

    • 每当 DataSource 中的数据发生变化时,handleChange 被触发。它调用 selectData 再次从 DataSource 获取最新的数据,并通过 setState 更新组件的 state,从而触发组件重新渲染。
  6. render 方法

    • render 中,WrappedComponent 被渲染并接收当前组件的 state.data 和所有 props。因此,WrappedComponent 能够使用被订阅的数据。

应用场景与示例

假设我们有一个 CommentList 组件,它需要动态显示某个用户的评论列表,使用 withSubscription 包装后,可以让它自动订阅数据源的变化并实时更新评论列表。

// selectComments 是一个从 DataSource 获取评论数据的函数
const selectComments = (DataSource, props) => {
    return DataSource.getCommentsByUserId(props.userId);
};

// 普通的组件
class CommentList extends React.Component {
    render() {
        const { data } = this.props; // 获取订阅的数据
        return (
            <ul>
                {data.map((comment) => (
                    <li key={comment.id}>{comment.text}</li>
                ))}
            </ul>
        );
    }
}

// 使用 withSubscription 高阶组件进行增强
const SubscribedCommentList = withSubscription(CommentList, selectComments);

export default SubscribedCommentList;

在这个示例中,SubscribedCommentList 是增强版的 CommentList,它会自动订阅数据源的变化,当 DataSource 中的评论数据发生变化时,组件会自动更新显示。

友情赞助
大家有空闲,帮忙试用下,国货抖音出品的AI编程代码插件,比比看GitHub Copilot那个好**^-^**
(适用JetBrains全家桶系列、Visual Studio家族IDE工具安装等主流IDE工具,支持100+种编程语言)
帮忙去助力>>