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

React 生命周期方法与异步操作的协调

2021-06-303.4k 阅读

React 生命周期方法概述

在深入探讨 React 生命周期方法与异步操作的协调之前,我们先来全面了解一下 React 生命周期方法。React 组件的生命周期分为三个主要阶段:挂载(Mounting)、更新(Updating)和卸载(Unmounting)。每个阶段都有特定的生命周期方法可供开发者使用,以在组件的不同阶段执行相应的逻辑。

挂载阶段

  1. constructor(props): 这是 ES6 类组件的构造函数。在这里,我们可以初始化组件的状态(state),并绑定事件处理函数。例如:
import React, { Component } from'react';

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            data: null
        };
        this.handleClick = this.handleClick.bind(this);
    }
    handleClick() {
        // 处理点击逻辑
    }
    render() {
        return <button onClick={this.handleClick}>Click Me</button>;
    }
}

注意,在调用 this.state 之前,必须先调用 super(props),因为 super(props) 会将 props 传递给父类的构造函数,从而正确初始化 this.props

  1. static getDerivedStateFromProps(props, state): 这是一个静态方法,在组件挂载和更新时都会被调用。它的作用是根据 props 的变化来更新 state。例如,当父组件传递的 props 发生变化,而子组件需要根据新的 props 更新自己的 state 时,可以使用这个方法。
import React, { Component } from'react';

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            displayValue: props.initialValue
        };
    }
    static getDerivedStateFromProps(props, state) {
        if (props.initialValue!== state.displayValue) {
            return {
                displayValue: props.initialValue
            };
        }
        return null;
    }
    render() {
        return <div>{this.state.displayValue}</div>;
    }
}

这个方法返回一个对象来更新 state,如果不需要更新 state,则返回 null

  1. render(): 这是组件中唯一必需的方法。它负责返回 React 元素,描述组件应该呈现的内容。render 方法应该是纯函数,即不应该改变组件的状态,也不应该与浏览器 API 进行交互。例如:
import React, { Component } from'react';

class MyComponent extends Component {
    render() {
        return <div>Hello, React!</div>;
    }
}
  1. componentDidMount(): 当组件被插入到 DOM 中后,这个方法会被调用。这是一个适合发起异步操作(如 API 请求)的地方,因为此时组件已经在 DOM 中,我们可以安全地操作 DOM 或进行需要 DOM 存在的初始化工作。例如:
import React, { Component } from'react';

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            data: null
        };
    }
    componentDidMount() {
        fetch('https://example.com/api/data')
          .then(response => response.json())
          .then(data => this.setState({ data }));
    }
    render() {
        return (
            <div>
                {this.state.data? <p>{JSON.stringify(this.state.data)}</p> : <p>Loading...</p>}
            </div>
        );
    }
}

更新阶段

  1. shouldComponentUpdate(nextProps, nextState): 这个方法在组件接收到新的 propsstate 时被调用,它允许开发者决定组件是否需要更新。返回 true 表示组件应该更新,返回 false 则表示组件不需要更新。这对于性能优化非常重要,因为不必要的更新会浪费资源。例如:
import React, { Component } from'react';

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }
    shouldComponentUpdate(nextProps, nextState) {
        if (nextState.count!== this.state.count) {
            return true;
        }
        return false;
    }
    handleClick() {
        this.setState(prevState => ({
            count: prevState.count + 1
        }));
    }
    render() {
        return (
            <div>
                <button onClick={() => this.handleClick()}>Increment</button>
                <p>{this.state.count}</p>
            </div>
        );
    }
}

在这个例子中,只有当 state 中的 count 发生变化时,组件才会更新。

  1. static getDerivedStateFromProps(props, state): 如前文所述,在更新阶段,这个方法同样会被调用,用于根据新的 props 更新 state

  2. render(): 在更新阶段,render 方法会再次被调用,以根据新的 propsstate 重新渲染组件。

  3. getSnapshotBeforeUpdate(prevProps, prevState): 这个方法在 render 之后、componentDidUpdate 之前被调用。它可以用来获取 DOM 变化之前的一些信息,例如滚动位置。返回的值会作为 componentDidUpdate 的第三个参数传递。例如:

import React, { Component } from'react';

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            list: [1, 2, 3]
        };
    }
    handleClick() {
        this.setState(prevState => ({
            list: [...prevState.list, prevState.list.length + 1]
        }));
    }
    getSnapshotBeforeUpdate(prevProps, prevState) {
        const list = this.refs.list;
        return list.scrollHeight - list.scrollTop;
    }
    componentDidUpdate(prevProps, prevState, snapshot) {
        const list = this.refs.list;
        list.scrollTop = list.scrollHeight - snapshot;
    }
    render() {
        return (
            <div>
                <button onClick={() => this.handleClick()}>Add Item</button>
                <ul ref="list">
                    {this.state.list.map(item => (
                        <li key={item}>{item}</li>
                    ))}
                </ul>
            </div>
        );
    }
}

在这个例子中,getSnapshotBeforeUpdate 获取了列表滚动条距离底部的距离,componentDidUpdate 则利用这个信息保持滚动位置不变。

  1. componentDidUpdate(prevProps, prevState, snapshot): 在组件更新后,这个方法会被调用。可以在这里执行依赖于 DOM 更新的操作,或者根据新的 propsstate 发起新的异步操作。例如:
import React, { Component } from'react';

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            data: null
        };
    }
    componentDidMount() {
        this.fetchData();
    }
    componentDidUpdate(prevProps) {
        if (this.props.filter!== prevProps.filter) {
            this.fetchData();
        }
    }
    fetchData() {
        fetch(`https://example.com/api/data?filter=${this.props.filter}`)
          .then(response => response.json())
          .then(data => this.setState({ data }));
    }
    render() {
        return (
            <div>
                {this.state.data? <p>{JSON.stringify(this.state.data)}</p> : <p>Loading...</p>}
            </div>
        );
    }
}

在这个例子中,当 props 中的 filter 发生变化时,组件会发起新的 API 请求。

卸载阶段

componentWillUnmount(): 当组件从 DOM 中移除时,这个方法会被调用。可以在这里执行清理操作,例如取消未完成的异步操作、解绑事件监听器等。例如:

import React, { Component } from'react';

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.timer = null;
    }
    componentDidMount() {
        this.timer = setInterval(() => {
            console.log('Component is mounted');
        }, 1000);
    }
    componentWillUnmount() {
        clearInterval(this.timer);
    }
    render() {
        return <div>Component with timer</div>;
    }
}

在这个例子中,componentWillUnmount 方法清除了在 componentDidMount 中设置的定时器,避免内存泄漏。

异步操作在 React 中的常见场景

API 请求

在前端开发中,与后端 API 进行数据交互是非常常见的需求。React 组件通常需要在挂载时获取初始数据,或者在 propsstate 变化时更新数据。例如,一个展示用户列表的组件,可能需要在组件挂载时从 API 获取用户数据:

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(users => this.setState({ users }));
    }
    render() {
        return (
            <ul>
                {this.state.users.map(user => (
                    <li key={user.id}>{user.name}</li>
                ))}
            </ul>
        );
    }
}

定时器

定时器在 React 中也经常被使用,例如实现自动刷新数据、动画效果等。比如,一个倒计时组件:

import React, { Component } from'react';

class Countdown extends Component {
    constructor(props) {
        super(props);
        this.state = {
            timeLeft: this.props.initialTime
        };
        this.timer = null;
    }
    componentDidMount() {
        this.timer = setInterval(() => {
            this.setState(prevState => ({
                timeLeft: prevState.timeLeft - 1
            }));
            if (prevState.timeLeft === 1) {
                clearInterval(this.timer);
            }
        }, 1000);
    }
    componentWillUnmount() {
        clearInterval(this.timer);
    }
    render() {
        return <p>{this.state.timeLeft} seconds left</p>;
    }
}

处理 Promise 链

在处理复杂的异步逻辑时,可能需要使用 Promise 链来顺序执行多个异步操作。例如,先获取用户信息,然后根据用户信息获取用户的订单:

import React, { Component } from'react';

class UserOrders extends Component {
    constructor(props) {
        super(props);
        this.state = {
            orders: []
        };
    }
    componentDidMount() {
        fetch('https://example.com/api/user')
          .then(response => response.json())
          .then(user => {
                return fetch(`https://example.com/api/orders?userId=${user.id}`);
            })
          .then(response => response.json())
          .then(orders => this.setState({ orders }));
    }
    render() {
        return (
            <ul>
                {this.state.orders.map(order => (
                    <li key={order.id}>{order.product}</li>
                ))}
            </ul>
        );
    }
}

生命周期方法与异步操作的协调问题

异步操作与挂载阶段

componentDidMount 中发起异步操作是很常见的,但如果在异步操作完成之前组件被卸载了,就可能会出现问题。例如,在一个模态框组件中,当用户关闭模态框时,组件会被卸载。如果在 componentDidMount 中发起的 API 请求还未完成,而 setState 操作在组件卸载后被调用,就会导致错误。

import React, { Component } from'react';

class Modal extends Component {
    constructor(props) {
        super(props);
        this.state = {
            data: null
        };
    }
    componentDidMount() {
        fetch('https://example.com/api/data')
          .then(response => response.json())
          .then(data => this.setState({ data }));
    }
    handleClose() {
        // 关闭模态框逻辑,这里会卸载组件
    }
    render() {
        return (
            <div>
                {this.state.data? <p>{JSON.stringify(this.state.data)}</p> : <p>Loading...</p>}
                <button onClick={this.handleClose}>Close</button>
            </div>
        );
    }
}

为了解决这个问题,可以在组件卸载时取消异步操作。对于 fetch 请求,可以使用 AbortController

import React, { Component } from'react';

class Modal extends Component {
    constructor(props) {
        super(props);
        this.state = {
            data: null
        };
        this.controller = new AbortController();
    }
    componentDidMount() {
        const { signal } = this.controller;
        fetch('https://example.com/api/data', { signal })
          .then(response => response.json())
          .then(data => this.setState({ data }))
          .catch(error => {
                if (error.name === 'AbortError') {
                    console.log('Request aborted');
                } else {
                    console.error('Request error:', error);
                }
            });
    }
    componentWillUnmount() {
        this.controller.abort();
    }
    handleClose() {
        // 关闭模态框逻辑,这里会卸载组件
    }
    render() {
        return (
            <div>
                {this.state.data? <p>{JSON.stringify(this.state.data)}</p> : <p>Loading...</p>}
                <button onClick={this.handleClose}>Close</button>
            </div>
        );
    }
}

异步操作与更新阶段

在更新阶段,componentDidUpdate 是一个容易出现问题的地方。如果在 componentDidUpdate 中发起异步操作,并且异步操作依赖于 prevPropsprevState,而在异步操作完成之前 propsstate 又发生了变化,就可能导致数据不一致。例如,一个搜索组件,当用户输入关键词时,组件会根据关键词发起 API 请求:

import React, { Component } from'react';

class SearchComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []
        };
    }
    componentDidUpdate(prevProps) {
        if (this.props.searchTerm!== prevProps.searchTerm) {
            fetch(`https://example.com/api/search?term=${this.props.searchTerm}`)
              .then(response => response.json())
              .then(results => this.setState({ results }));
        }
    }
    render() {
        return (
            <div>
                <input type="text" value={this.props.searchTerm} onChange={this.props.onChange} />
                <ul>
                    {this.state.results.map(result => (
                        <li key={result.id}>{result.title}</li>
                    ))}
                </ul>
            </div>
        );
    }
}

假设用户快速输入多个关键词,每次输入都会触发 componentDidUpdate 中的 API 请求。由于网络延迟,后发起的请求可能先完成,导致显示的搜索结果与当前输入的关键词不匹配。为了解决这个问题,可以使用一个标志变量来跟踪当前请求是否是最新的:

import React, { Component } from'react';

class SearchComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []
        };
        this.currentRequestId = 0;
    }
    componentDidUpdate(prevProps) {
        if (this.props.searchTerm!== prevProps.searchTerm) {
            this.currentRequestId++;
            const requestId = this.currentRequestId;
            fetch(`https://example.com/api/search?term=${this.props.searchTerm}`)
              .then(response => response.json())
              .then(results => {
                    if (requestId === this.currentRequestId) {
                        this.setState({ results });
                    }
                });
        }
    }
    render() {
        return (
            <div>
                <input type="text" value={this.props.searchTerm} onChange={this.props.onChange} />
                <ul>
                    {this.state.results.map(result => (
                        <li key={result.id}>{result.title}</li>
                    ))}
                </ul>
            </div>
        );
    }
}

异步操作与卸载阶段

在卸载阶段,确保清理所有异步操作是至关重要的,以避免内存泄漏。除了前文提到的取消 fetch 请求和清除定时器外,对于一些自定义的异步任务,也需要进行相应的清理。例如,使用 WebSocket 进行实时通信的组件,在组件卸载时需要关闭 WebSocket 连接:

import React, { Component } from'react';

class WebSocketComponent extends Component {
    constructor(props) {
        super(props);
        this.socket = new WebSocket('wss://example.com/socket');
        this.socket.onmessage = this.handleMessage.bind(this);
    }
    handleMessage(event) {
        const data = JSON.parse(event.data);
        // 处理接收到的数据
    }
    componentWillUnmount() {
        this.socket.close();
    }
    render() {
        return <div>WebSocket Component</div>;
    }
}

优化异步操作与生命周期方法的协调

使用 async/await

async/await 语法可以使异步代码看起来更像同步代码,从而提高代码的可读性和可维护性。例如,在 componentDidMount 中发起多个 API 请求:

import React, { Component } from'react';

class DataComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            user: null,
            orders: []
        };
    }
    async componentDidMount() {
        try {
            const userResponse = await fetch('https://example.com/api/user');
            const user = await userResponse.json();
            const ordersResponse = await fetch(`https://example.com/api/orders?userId=${user.id}`);
            const orders = await ordersResponse.json();
            this.setState({ user, orders });
        } catch (error) {
            console.error('Error fetching data:', error);
        }
    }
    render() {
        return (
            <div>
                {this.state.user && (
                    <p>User: {this.state.user.name}</p>
                )}
                <ul>
                    {this.state.orders.map(order => (
                        <li key={order.id}>{order.product}</li>
                    ))}
                </ul>
            </div>
        );
    }
}

采用状态管理库

对于复杂的异步操作和组件间状态共享,使用状态管理库(如 Redux 或 MobX)可以更好地协调异步操作与生命周期方法。例如,在 Redux 中,可以使用 redux - thunkredux - saga 来处理异步操作。以 redux - thunk 为例:

// actions.js
import { FETCH_USER_SUCCESS, FETCH_USER_FAILURE } from './actionTypes';
import axios from 'axios';

export const fetchUser = () => {
    return async (dispatch) => {
        try {
            const response = await axios.get('https://example.com/api/user');
            dispatch({ type: FETCH_USER_SUCCESS, payload: response.data });
        } catch (error) {
            dispatch({ type: FETCH_USER_FAILURE, payload: error.message });
        }
    };
};

// reducer.js
import { FETCH_USER_SUCCESS, FETCH_USER_FAILURE } from './actionTypes';

const initialState = {
    user: null,
    error: null
};

const userReducer = (state = initialState, action) => {
    switch (action.type) {
        case FETCH_USER_SUCCESS:
            return {
               ...state,
                user: action.payload,
                error: null
            };
        case FETCH_USER_FAILURE:
            return {
               ...state,
                user: null,
                error: action.payload
            };
        default:
            return state;
    }
};

export default userReducer;

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

class UserComponent extends Component {
    componentDidMount() {
        this.props.fetchUser();
    }
    render() {
        const { user, error } = this.props;
        return (
            <div>
                {error && <p>{error}</p>}
                {user && <p>User: {user.name}</p>}
            </div>
        );
    }
}

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

const mapDispatchToProps = {
    fetchUser
};

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

节流与防抖

在处理用户输入等频繁触发的事件时,节流(throttle)和防抖(debounce)技术可以减少不必要的异步操作。例如,在搜索框输入时,使用防抖可以避免频繁发起 API 请求:

import React, { Component } from'react';
import _ from 'lodash';

class DebounceSearch extends Component {
    constructor(props) {
        super(props);
        this.state = {
            results: []
        };
        this.debouncedSearch = _.debounce(this.search.bind(this), 300);
    }
    search() {
        const { searchTerm } = this.props;
        fetch(`https://example.com/api/search?term=${searchTerm}`)
          .then(response => response.json())
          .then(results => this.setState({ results }));
    }
    componentDidMount() {
        this.debouncedSearch();
    }
    componentWillUnmount() {
        this.debouncedSearch.cancel();
    }
    handleChange = (e) => {
        this.props.onChange(e.target.value);
        this.debouncedSearch();
    }
    render() {
        return (
            <div>
                <input type="text" value={this.props.searchTerm} onChange={this.handleChange} />
                <ul>
                    {this.state.results.map(result => (
                        <li key={result.id}>{result.title}</li>
                    ))}
                </ul>
            </div>
        );
    }
}

在这个例子中,_.debounce 函数确保 search 方法在用户停止输入 300 毫秒后才会被调用,从而减少了 API 请求的频率。

通过合理运用这些方法,我们可以更好地协调 React 生命周期方法与异步操作,提高应用的性能和稳定性。无论是简单的组件还是复杂的大型应用,对异步操作的有效管理都是前端开发中不可或缺的一部分。在实际开发中,需要根据具体的业务需求和场景选择合适的方法来处理异步操作与生命周期的关系,以实现高效、可靠的前端应用。同时,随着 React 技术的不断发展,新的特性和工具也会不断涌现,开发者需要持续学习和关注,以适应不断变化的技术环境。在处理异步操作时,始终要牢记性能优化和用户体验,确保应用在各种情况下都能提供流畅的交互。