MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 使用高阶组件封装生命周期逻辑

2023-03-193.5k 阅读

一、高阶组件基础概念

在深入探讨如何使用高阶组件封装 React 生命周期逻辑之前,我们先来明确一下高阶组件(Higher - Order Component,简称 HOC)的基本概念。

高阶组件本身并不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。简单来说,高阶组件是一个函数,它接受一个组件作为参数,并返回一个新的组件。

用代码来表示,其基本结构如下:

import React from'react';

// 高阶组件函数
const withSomeEnhancement = (WrappedComponent) => {
    return class EnhancedComponent extends React.Component {
        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
};

// 使用高阶组件
class MyComponent extends React.Component {
    render() {
        return <div>My Component</div>;
    }
}

const EnhancedMyComponent = withSomeEnhancement(MyComponent);

在上述代码中,withSomeEnhancement 就是一个高阶组件,它接受 MyComponent 作为参数,并返回一个新的组件 EnhancedMyComponent。新组件 EnhancedMyComponent 内部渲染了传入的 WrappedComponent,并将自身的 props 透传给它。

二、React 生命周期简介

React 组件拥有一系列的生命周期方法,这些方法在组件的不同阶段被调用,帮助我们管理组件的状态、处理副作用等。

  1. 挂载阶段

    • constructor:组件实例化时调用,通常用于初始化 state 和绑定方法。
    • componentWillMount:在组件即将被插入到 DOM 之前调用,这个方法在 React v17 及之后版本已被弃用。
    • render:必需方法,用于返回要渲染的 JSX 元素。
    • componentDidMount:组件被插入到 DOM 之后调用,适合进行副作用操作,如网络请求、添加事件监听器等。
  2. 更新阶段

    • componentWillReceiveProps:在组件接收到新的 props 时调用,这个方法在 React v17 及之后版本已被弃用。
    • shouldComponentUpdate:根据返回值决定组件是否需要更新,返回 true 表示更新,false 表示不更新,有助于性能优化。
    • componentWillUpdate:在组件即将更新时调用,此方法在 React v17 及之后版本已被弃用。
    • render:再次调用以返回新的 JSX 元素。
    • componentDidUpdate:在组件更新完成后调用,适合在更新后执行一些操作,如更新 DOM 元素的样式等。
  3. 卸载阶段

    • componentWillUnmount:在组件即将从 DOM 中移除时调用,适合清理副作用,如移除事件监听器、取消网络请求等。

三、为什么要用高阶组件封装生命周期逻辑

  1. 代码复用 在多个组件中,可能会有相同的生命周期逻辑。例如,多个组件都需要在挂载时发起网络请求,在卸载时取消请求。如果每个组件都重复编写这些逻辑,代码会变得冗长且难以维护。通过高阶组件封装这些通用的生命周期逻辑,可以将其复用在多个组件上。

  2. 关注点分离 将生命周期逻辑从业务组件中分离出来,使得业务组件更加专注于自身的业务逻辑,而高阶组件专注于处理通用的生命周期相关的操作。这样使得代码结构更加清晰,便于理解和维护。

  3. 增强组件功能 通过高阶组件,可以在不修改原有组件代码的基础上,为组件添加新的生命周期相关的功能。例如,为组件添加日志记录功能,在组件的某些生命周期方法执行时记录日志。

四、使用高阶组件封装挂载和卸载逻辑

  1. 封装挂载时的网络请求 假设我们有多个组件需要在挂载时发起网络请求获取数据。我们可以创建一个高阶组件来封装这个逻辑。

首先,创建一个 fetchData 函数用于模拟网络请求:

const fetchData = () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ data: 'Mocked response' });
        }, 1000);
    });
};

然后,创建高阶组件 withDataFetching

import React from'react';

const withDataFetching = (WrappedComponent) => {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                data: null,
                isLoading: false
            };
        }

        async componentDidMount() {
            this.setState({ isLoading: true });
            const response = await fetchData();
            this.setState({ data: response.data, isLoading: false });
        }

        render() {
            return <WrappedComponent {...this.props} data={this.state.data} isLoading={this.state.isLoading} />;
        }
    };
};

现在,我们可以使用这个高阶组件来为其他组件添加数据获取功能:

class MyDataComponent extends React.Component {
    render() {
        const { data, isLoading } = this.props;
        return (
            <div>
                {isLoading? <p>Loading...</p> : <p>{data}</p>}
            </div>
        );
    }
}

const EnhancedDataComponent = withDataFetching(MyDataComponent);
  1. 封装卸载时的清理操作 有时候,我们在组件挂载时添加了一些事件监听器或者启动了一些定时器,需要在组件卸载时进行清理。

创建一个高阶组件 withCleanup

import React from'react';

const withCleanup = (WrappedComponent) => {
    return class extends React.Component {
        componentDidMount() {
            // 模拟添加事件监听器
            window.addEventListener('resize', this.handleResize);
        }

        componentWillUnmount() {
            // 模拟移除事件监听器
            window.removeEventListener('resize', this.handleResize);
        }

        handleResize = () => {
            console.log('Window resized');
        }

        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
};

使用这个高阶组件:

class MyComponentWithCleanup extends React.Component {
    render() {
        return <div>Component with cleanup</div>;
    }
}

const EnhancedCleanupComponent = withCleanup(MyComponentWithCleanup);

五、使用高阶组件封装更新逻辑

  1. 在组件更新时进行数据比对 假设我们有一个组件,当 props 中的某个特定属性发生变化时,我们需要进行一些特殊的处理。我们可以通过高阶组件来封装这个更新逻辑。

创建高阶组件 withPropChangeCheck

import React from'react';

const withPropChangeCheck = (WrappedComponent, propToCheck) => {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.prevPropValue = props[propToCheck];
        }

        componentDidUpdate(prevProps) {
            if (prevProps[propToCheck]!== this.props[propToCheck]) {
                console.log(`${propToCheck} has changed`);
                // 这里可以进行特殊处理,如重新计算数据等
            }
        }

        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
};

使用这个高阶组件:

class MyPropChangeComponent extends React.Component {
    render() {
        return <div>{this.props.value}</div>;
    }
}

const EnhancedPropChangeComponent = withPropChangeCheck(MyPropChangeComponent, 'value');
  1. 条件更新组件 有时候,我们希望根据某些条件来决定组件是否更新。可以通过高阶组件来实现这个逻辑。

创建高阶组件 conditionalUpdate

import React from'react';

const conditionalUpdate = (WrappedComponent, condition) => {
    return class extends React.Component {
        shouldComponentUpdate(nextProps, nextState) {
            return condition(nextProps, nextState, this.props, this.state);
        }

        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
};

使用这个高阶组件:

const updateCondition = (nextProps, nextState, prevProps, prevState) => {
    return nextProps.value!== prevProps.value;
};

class MyConditionalUpdateComponent extends React.Component {
    render() {
        return <div>{this.props.value}</div>;
    }
}

const EnhancedConditionalUpdateComponent = conditionalUpdate(MyConditionalUpdateComponent, updateCondition);

六、高阶组件的组合使用

在实际应用中,我们可能需要为一个组件同时添加多种高阶组件带来的功能。例如,一个组件既需要在挂载时获取数据,又需要在卸载时进行清理操作,还需要在 props 变化时进行特殊处理。

我们可以将多个高阶组件组合使用:

import React from'react';

const withDataFetching = (WrappedComponent) => {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                data: null,
                isLoading: false
            };
        }

        async componentDidMount() {
            this.setState({ isLoading: true });
            const response = await fetchData();
            this.setState({ data: response.data, isLoading: false });
        }

        render() {
            return <WrappedComponent {...this.props} data={this.state.data} isLoading={this.state.isLoading} />;
        }
    };
};

const withCleanup = (WrappedComponent) => {
    return class extends React.Component {
        componentDidMount() {
            window.addEventListener('resize', this.handleResize);
        }

        componentWillUnmount() {
            window.removeEventListener('resize', this.handleResize);
        }

        handleResize = () => {
            console.log('Window resized');
        }

        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
};

const withPropChangeCheck = (WrappedComponent, propToCheck) => {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.prevPropValue = props[propToCheck];
        }

        componentDidUpdate(prevProps) {
            if (prevProps[propToCheck]!== this.props[propToCheck]) {
                console.log(`${propToCheck} has changed`);
            }
        }

        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
};

class MyComplexComponent extends React.Component {
    render() {
        return <div>My Complex Component</div>;
    }
}

const EnhancedComplexComponent = withPropChangeCheck(withCleanup(withDataFetching(MyComplexComponent)), 'value');

在上述代码中,MyComplexComponent 依次被 withDataFetchingwithCleanupwithPropChangeCheck 三个高阶组件增强,从而具备了获取数据、清理操作以及 props 变化检查的功能。

七、高阶组件的注意事项

  1. props 透传问题 在高阶组件内部,要确保将所有的 props 正确地透传给被包裹的组件。如果高阶组件意外地覆盖了某些 props,可能会导致被包裹组件的行为异常。

  2. 静态方法丢失 当使用高阶组件包裹一个组件时,被包裹组件的静态方法不会被自动继承到新的组件上。如果需要保留静态方法,可以手动将其复制到新组件上。

例如:

import React from'react';

const withSomeEnhancement = (WrappedComponent) => {
    return class EnhancedComponent extends React.Component {
        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
};

class MyComponent extends React.Component {
    static someStaticMethod() {
        return 'Static method result';
    }

    render() {
        return <div>My Component</div>;
    }
}

const EnhancedMyComponent = withSomeEnhancement(MyComponent);

// 手动复制静态方法
EnhancedMyComponent.someStaticMethod = MyComponent.someStaticMethod;
  1. 命名问题 为了便于调试和理解,给高阶组件返回的新组件命名时,最好能体现出其来源和功能。可以使用 displayName 属性来设置组件的显示名称。

例如:

const withSomeEnhancement = (WrappedComponent) => {
    return class EnhancedComponent extends React.Component {
        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
};

class MyComponent extends React.Component {
    render() {
        return <div>My Component</div>;
    }
}

const EnhancedMyComponent = withSomeEnhancement(MyComponent);

EnhancedMyComponent.displayName = `withSomeEnhancement(${MyComponent.displayName || 'MyComponent'})`;

通过以上方法,我们可以更加合理、有效地使用高阶组件来封装 React 组件的生命周期逻辑,提高代码的复用性、可维护性和可扩展性。在实际项目中,根据具体的业务需求,灵活运用高阶组件的特性,能够大大提升开发效率和代码质量。