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

React 生命周期中的数据加载策略

2023-12-177.7k 阅读

React 生命周期概述

在深入探讨 React 生命周期中的数据加载策略之前,我们先来回顾一下 React 的生命周期。React 组件的生命周期分为三个主要阶段:挂载(Mounting)、更新(Updating)和卸载(Unmounting)。

  1. 挂载阶段:当组件实例被创建并插入 DOM 中时,会依次调用以下方法:

    • constructor():组件的构造函数,在组件实例化时被调用。通常用于初始化 state 和绑定方法。
    • getDerivedStateFromProps():这个静态方法在组件挂载和更新时都会被调用。它接收新的 props 和当前的 state,返回一个对象来更新 state,或者返回 null 表示不需要更新。
    • render():这是组件中唯一必需的方法。它返回一个 React 元素,描述了组件应该如何渲染到 DOM 中。
    • componentDidMount():在组件被插入 DOM 后立即调用。这是进行副作用操作(如数据加载、订阅事件等)的好地方。
  2. 更新阶段:当组件的 props 或 state 发生变化时,会触发更新过程,依次调用以下方法:

    • getDerivedStateFromProps():同挂载阶段,用于根据新的 props 更新 state。
    • shouldComponentUpdate():这个方法接收新的 props 和 state,返回一个布尔值,决定组件是否应该更新。返回 false 可以阻止不必要的更新,提高性能。
    • render():再次渲染组件。
    • getSnapshotBeforeUpdate():在 render 之后,更新 DOM 之前被调用。它可以返回一个值,这个值会作为参数传递给 componentDidUpdate()
    • componentDidUpdate():在组件更新后被调用。可以在这里进行依赖于 DOM 更新的操作,如操作新的 DOM 元素、更新第三方库等。
  3. 卸载阶段:当组件从 DOM 中移除时,会调用 componentWillUnmount() 方法。可以在这里清理副作用,如取消网络请求、移除事件监听器等。

挂载阶段的数据加载

在 React 组件的挂载阶段进行数据加载是一种常见的策略。通常,我们会在 componentDidMount() 方法中发起数据请求。

示例:使用 fetch 进行数据加载

假设我们有一个展示用户列表的组件,我们需要从服务器获取用户数据并展示。

import React, { Component } from'react';

class UserList extends Component {
    constructor(props) {
        super(props);
        this.state = {
            users: []
        };
    }

    componentDidMount() {
        fetch('https://example.com/api/users')
          .then(response => response.json())
          .then(data => {
                this.setState({ users: data });
            });
    }

    render() {
        return (
            <ul>
                {this.state.users.map(user => (
                    <li key={user.id}>{user.name}</li>
                ))}
            </ul>
        );
    }
}

export default UserList;

在上述代码中,componentDidMount() 方法在组件挂载到 DOM 后立即执行。通过 fetch 发起网络请求,获取用户数据。当数据获取成功后,使用 setState 更新组件的 state,从而触发组件重新渲染,展示用户列表。

处理加载状态

在数据加载过程中,我们通常需要向用户展示加载状态,以提供更好的用户体验。可以通过在 state 中添加一个标志位来表示加载状态。

import React, { Component } from'react';

class UserList extends Component {
    constructor(props) {
        super(props);
        this.state = {
            users: [],
            isLoading: false
        };
    }

    componentDidMount() {
        this.setState({ isLoading: true });
        fetch('https://example.com/api/users')
          .then(response => response.json())
          .then(data => {
                this.setState({ users: data, isLoading: false });
            });
    }

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

        return (
            <ul>
                {this.state.users.map(user => (
                    <li key={user.id}>{user.name}</li>
                ))}
            </ul>
        );
    }
}

export default UserList;

在这个更新后的代码中,isLoading 标志位在发起请求时设为 true,数据加载完成后设为 false。在 render 方法中,根据 isLoading 的值决定是显示加载提示还是用户列表。

更新阶段的数据加载

在组件更新阶段,数据加载的需求可能会因为 props 的变化而产生。例如,一个根据不同用户 ID 展示用户详情的组件,当 props 中的用户 ID 变化时,需要重新加载对应的用户详情数据。

根据 props 变化加载数据

import React, { Component } from'react';

class UserDetail extends Component {
    constructor(props) {
        super(props);
        this.state = {
            user: null,
            isLoading: false
        };
    }

    componentDidMount() {
        this.fetchUser();
    }

    componentDidUpdate(prevProps) {
        if (prevProps.userId!== this.props.userId) {
            this.fetchUser();
        }
    }

    fetchUser() {
        this.setState({ isLoading: true });
        const { userId } = this.props;
        fetch(`https://example.com/api/users/${userId}`)
          .then(response => response.json())
          .then(data => {
                this.setState({ user: data, isLoading: false });
            });
    }

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

        if (!this.state.user) {
            return null;
        }

        return (
            <div>
                <h2>{this.state.user.name}</h2>
                <p>{this.state.user.email}</p>
            </div>
        );
    }
}

export default UserDetail;

在上述代码中,componentDidMount() 方法在组件首次挂载时发起用户数据请求。componentDidUpdate() 方法会在组件更新时被调用,通过比较 prevPropscurrentProps 中的 userId,如果 userId 发生变化,则重新调用 fetchUser() 方法获取新用户的数据。

使用 getDerivedStateFromProps 进行数据加载

虽然 getDerivedStateFromProps 更多用于根据 props 更新 state,但在某些情况下也可以结合数据加载逻辑。不过需要注意,由于它是静态方法,不能直接访问 this,所以通常需要配合一个辅助函数来完成数据加载。

import React, { Component } from'react';

class UserDetail extends Component {
    constructor(props) {
        super(props);
        this.state = {
            user: null,
            isLoading: false
        };
    }

    static getDerivedStateFromProps(props, state) {
        if (props.userId!== (state.lastUserId || null)) {
            return { isLoading: true, lastUserId: props.userId };
        }
        return null;
    }

    componentDidUpdate(prevProps, prevState) {
        if (prevProps.userId!== this.props.userId) {
            this.fetchUser();
        }
    }

    fetchUser() {
        const { userId } = this.props;
        fetch(`https://example.com/api/users/${userId}`)
          .then(response => response.json())
          .then(data => {
                this.setState({ user: data, isLoading: false });
            });
    }

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

        if (!this.state.user) {
            return null;
        }

        return (
            <div>
                <h2>{this.state.user.name}</h2>
                <p>{this.state.user.email}</p>
            </div>
        );
    }
}

export default UserDetail;

在这个例子中,getDerivedStateFromProps 方法检查 props.userId 是否发生变化,如果变化则更新 isLoading 状态为 true 并记录新的 userId。然后在 componentDidUpdate 中根据 props 的变化发起实际的数据请求。

数据加载与 shouldComponentUpdate

shouldComponentUpdate 方法对于优化数据加载和组件更新性能至关重要。它可以让我们根据 props 和 state 的变化来决定组件是否真的需要更新。

避免不必要的数据加载

假设我们有一个组件,它接收一个 filter prop 来筛选数据列表。当 filter 没有变化时,我们不希望重新加载数据。

import React, { Component } from'react';

class FilteredList extends Component {
    constructor(props) {
        super(props);
        this.state = {
            items: [],
            isLoading: false
        };
    }

    componentDidMount() {
        this.fetchItems();
    }

    componentDidUpdate(prevProps) {
        if (prevProps.filter!== this.props.filter) {
            this.fetchItems();
        }
    }

    shouldComponentUpdate(nextProps, nextState) {
        return nextProps.filter!== this.props.filter || nextState.isLoading!== this.state.isLoading;
    }

    fetchItems() {
        this.setState({ isLoading: true });
        const { filter } = this.props;
        fetch(`https://example.com/api/items?filter=${filter}`)
          .then(response => response.json())
          .then(data => {
                this.setState({ items: data, isLoading: false });
            });
    }

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

        return (
            <ul>
                {this.state.items.map(item => (
                    <li key={item.id}>{item.name}</li>
                ))}
            </ul>
        );
    }
}

export default FilteredList;

在上述代码中,shouldComponentUpdate 方法只在 filter prop 或 isLoading state 发生变化时返回 true,允许组件更新。这样可以避免在 filter 没有变化时不必要的数据加载和组件重新渲染,提高性能。

处理数据加载错误

在数据加载过程中,难免会遇到各种错误,如网络故障、服务器错误等。我们需要在 React 组件中妥善处理这些错误,以提供良好的用户体验。

捕获并显示错误信息

import React, { Component } from'react';

class DataComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            data: null,
            isLoading: false,
            error: null
        };
    }

    componentDidMount() {
        this.fetchData();
    }

    fetchData() {
        this.setState({ isLoading: true });
        fetch('https://example.com/api/data')
          .then(response => {
                if (!response.ok) {
                    throw new Error('Network response was not ok');
                }
                return response.json();
            })
          .then(data => {
                this.setState({ data, isLoading: false, error: null });
            })
          .catch(error => {
                this.setState({ isLoading: false, error: error.message });
            });
    }

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

        if (this.state.error) {
            return <div>{`Error: ${this.state.error}`}</div>;
        }

        if (!this.state.data) {
            return null;
        }

        return (
            <div>
                <h2>Loaded Data</h2>
                <p>{this.state.data.info}</p>
            </div>
        );
    }
}

export default DataComponent;

在这个例子中,fetch 请求过程中,如果响应状态码不是 ok,则抛出错误。在 catch 块中捕获错误,并将错误信息更新到 state 中。在 render 方法中,根据 error state 是否存在来显示相应的错误提示。

数据加载优化策略

  1. 数据缓存:在组件内部或全局状态管理工具(如 Redux)中缓存已加载的数据。当再次需要相同数据时,先检查缓存,避免重复请求。
    • 示例
import React, { Component } from'react';

class CachedDataComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            data: null,
            isLoading: false,
            cache: {}
        };
    }

    componentDidMount() {
        this.fetchData();
    }

    fetchData() {
        const { key } = this.props;
        if (this.state.cache[key]) {
            this.setState({ data: this.state.cache[key], isLoading: false });
            return;
        }

        this.setState({ isLoading: true });
        fetch(`https://example.com/api/data/${key}`)
          .then(response => response.json())
          .then(data => {
                this.setState({ data, isLoading: false });
                this.state.cache[key] = data;
            });
    }

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

        if (!this.state.data) {
            return null;
        }

        return (
            <div>
                <h2>Loaded Data</h2>
                <p>{this.state.data.info}</p>
            </div>
        );
    }
}

export default CachedDataComponent;
  1. 批量数据请求:如果组件需要从服务器获取多个相关的数据,可以将这些请求合并为一个请求,减少网络开销。
    • 示例:假设我们需要获取用户信息和用户的订单列表,可以设计一个 API 端点同时返回这两个数据。
import React, { Component } from'react';

class UserAndOrdersComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            user: null,
            orders: [],
            isLoading: false
        };
    }

    componentDidMount() {
        this.fetchData();
    }

    fetchData() {
        this.setState({ isLoading: true });
        fetch('https://example.com/api/userAndOrders')
          .then(response => response.json())
          .then(data => {
                this.setState({ user: data.user, orders: data.orders, isLoading: false });
            });
    }

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

        if (!this.state.user ||!this.state.orders.length) {
            return null;
        }

        return (
            <div>
                <h2>{this.state.user.name}</h2>
                <ul>
                    {this.state.orders.map(order => (
                        <li key={order.id}>{order.product}</li>
                    ))}
                </ul>
            </div>
        );
    }
}

export default UserAndOrdersComponent;
  1. 使用 Suspense 和懒加载:React.lazy 和 Suspense 可以实现组件的懒加载,延迟数据加载直到真正需要时。这在处理大型组件或数据量较大的组件时非常有用。
    • 示例
import React, { Suspense, lazy } from'react';

const BigDataComponent = lazy(() => import('./BigDataComponent'));

function App() {
    return (
        <Suspense fallback={<div>Loading...</div>}>
            <BigDataComponent />
        </Suspense>
    );
}

export default App;

BigDataComponent 内部,可以在合适的生命周期方法中进行数据加载,这样只有在组件即将渲染时才会触发数据加载。

不同 React 版本的数据加载变化

  1. React 16.3 之前:在早期版本中,componentWillMount 方法常被用于数据加载。然而,这个方法存在一些问题,比如在服务端渲染时可能会被调用多次,而且在异步渲染模式下可能会出现不一致的情况。
    • 示例
import React, { Component } from'react';

class OldDataLoadComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            data: null
        };
    }

    componentWillMount() {
        fetch('https://example.com/api/data')
          .then(response => response.json())
          .then(data => {
                this.setState({ data });
            });
    }

    render() {
        if (!this.state.data) {
            return null;
        }

        return (
            <div>
                <h2>Loaded Data</h2>
                <p>{this.state.data.info}</p>
            </div>
        );
    }
}

export default OldDataLoadComponent;
  1. React 16.3 及之后:随着 React 引入新的生命周期方法(如 getDerivedStateFromPropsgetSnapshotBeforeUpdate 等),推荐在 componentDidMountcomponentDidUpdate 中进行数据加载,以更好地适应 React 的新特性和优化策略。同时,componentWillMount 被标记为不安全的生命周期方法,在未来版本可能会被移除。

与第三方库结合的数据加载

  1. Axios:Axios 是一个流行的基于 Promise 的 HTTP 客户端,与 React 结合使用可以更方便地进行数据加载。它提供了丰富的配置选项和拦截器功能。
    • 示例
import React, { Component } from'react';
import axios from 'axios';

class AxiosDataComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            data: null,
            isLoading: false
        };
    }

    componentDidMount() {
        this.fetchData();
    }

    fetchData() {
        this.setState({ isLoading: true });
        axios.get('https://example.com/api/data')
          .then(response => {
                this.setState({ data: response.data, isLoading: false });
            })
          .catch(error => {
                this.setState({ isLoading: false, error: error.message });
            });
    }

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

        if (this.state.error) {
            return <div>{`Error: ${this.state.error}`}</div>;
        }

        if (!this.state.data) {
            return null;
        }

        return (
            <div>
                <h2>Loaded Data</h2>
                <p>{this.state.data.info}</p>
            </div>
        );
    }
}

export default AxiosDataComponent;
  1. Redux - Saga:Redux - Saga 是一个用于管理 Redux 应用副作用(如异步操作、数据加载)的库。它使用生成器函数来处理异步流程,使得代码更加可维护和测试。
    • 示例:首先,定义一个 saga 来处理数据加载。
import { call, put, takeEvery } from'redux - saga/effects';
import axios from 'axios';
import { FETCH_DATA_SUCCESS, FETCH_DATA_FAILURE } from './actionTypes';

function* fetchData() {
    try {
        const response = yield call(axios.get, 'https://example.com/api/data');
        yield put({ type: FETCH_DATA_SUCCESS, payload: response.data });
    } catch (error) {
        yield put({ type: FETCH_DATA_FAILURE, payload: error.message });
    }
}

export function* dataSaga() {
    yield takeEvery('FETCH_DATA_REQUEST', fetchData);
}

然后,在 React 组件中触发这个 saga。

import React, { Component } from'react';
import { connect } from'react-redux';
import { fetchDataRequest } from './actions';

class ReduxSagaDataComponent extends Component {
    componentDidMount() {
        this.props.fetchDataRequest();
    }

    render() {
        const { data, isLoading, error } = this.props;

        if (isLoading) {
            return <div>Loading...</div>;
        }

        if (error) {
            return <div>{`Error: ${error}`}</div>;
        }

        if (!data) {
            return null;
        }

        return (
            <div>
                <h2>Loaded Data</h2>
                <p>{data.info}</p>
            </div>
        );
    }
}

const mapStateToProps = state => ({
    data: state.data.data,
    isLoading: state.data.isLoading,
    error: state.data.error
});

const mapDispatchToProps = dispatch => ({
    fetchDataRequest: () => dispatch(fetchDataRequest())
});

export default connect(mapStateToProps, mapDispatchToProps)(ReduxSagaDataComponent);

通过 Redux - Saga,我们可以将数据加载逻辑从组件中分离出来,使得组件更加专注于 UI 展示,同时提供了更好的异步操作管理和错误处理机制。

结论

在 React 生命周期中选择合适的数据加载策略对于构建高效、用户友好的应用至关重要。通过合理利用挂载、更新阶段的生命周期方法,处理加载状态、错误,以及结合优化策略和第三方库,可以有效地提升应用的性能和用户体验。随着 React 的不断发展,我们需要持续关注新的特性和最佳实践,以确保我们的数据加载策略与时俱进。同时,在实际项目中,要根据具体的需求和场景,灵活选择和组合不同的数据加载方式,打造出优质的前端应用。