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

React 生命周期方法全解析

2023-02-046.7k 阅读

React 生命周期的概念与重要性

React 组件拥有生命周期,这一系列过程就像人类从出生到成长再到消亡。理解并正确运用生命周期方法,对于优化 React 应用的性能、管理资源以及处理复杂业务逻辑至关重要。它让开发者能够在组件生命的不同阶段执行特定操作,比如在组件创建时获取数据、在更新时重新计算,以及在销毁时清理资源。

React 生命周期的三个阶段

React 组件的生命周期主要分为三个阶段:挂载(Mounting)、更新(Updating)和卸载(Unmounting)。每个阶段都包含多个生命周期方法,开发者可以根据需求在不同方法中编写相应逻辑。

挂载阶段

  1. constructor()
    • 作用:构造函数,用于初始化 state 和绑定方法。在 React 组件挂载之前,首先会调用 constructor 方法。它是 ES6 类的构造函数,在 React 组件中,通常会在这个方法里初始化组件的 state。
    • 代码示例
import React, { Component } from'react';

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
        this.handleClick = this.handleClick.bind(this);
    }
    handleClick() {
        this.setState({
            count: this.state.count + 1
        });
    }
    render() {
        return (
            <div>
                <p>Count: {this.state.count}</p>
                <button onClick={this.handleClick}>Increment</button>
            </div>
        );
    }
}

export default MyComponent;
- **注意事项**:在 constructor 中调用 super(props) 是必要的,这样才能在 constructor 中使用 this.props。而且,尽量避免在 constructor 中进行复杂计算或 API 调用,因为 constructor 是同步执行的,可能会阻塞渲染。

2. static getDerivedStateFromProps(props, state) - 作用:这个静态方法在组件实例化之后,以及每次接收新的 props 时都会被调用。它的返回值会被用于更新 state。主要用于根据 props 来更新 state,并且在更新 state 时不会触发额外的渲染。 - 代码示例

import React, { Component } from'react';

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

export default MyComponent;
- **注意事项**:它是一个静态方法,不能在里面使用 this。而且返回 null 表示不需要更新 state,如果返回一个对象,则会将该对象合并到当前 state 中。由于它会在每次 props 更新时调用,所以要避免在此方法中进行副作用操作,如 API 调用等。

3. render() - 作用:这是 React 组件中唯一必需的方法。它用于描述组件的 UI 结构,返回一个 React 元素,如 JSX 或者 null、false 等。React 根据这个返回值来创建 DOM 树。 - 代码示例

import React, { Component } from'react';

class MyComponent extends Component {
    render() {
        return <div>Hello, React!</div>;
    }
}

export default MyComponent;
- **注意事项**:render 方法应该是纯函数,即不应该改变组件的 state,也不应该和浏览器 API 进行交互。它只负责返回 UI 结构,任何复杂逻辑都应该放在其他生命周期方法或自定义方法中。

4. componentDidMount() - 作用:在组件挂载到 DOM 后立即调用。这个方法通常用于执行需要 DOM 节点的操作,如初始化第三方库(如 jQuery 插件)、订阅事件、发起网络请求等。 - 代码示例

import React, { Component } from'react';

class MyComponent extends Component {
    componentDidMount() {
        console.log('Component has been mounted to the DOM');
        // 模拟网络请求
        fetch('https://example.com/api/data')
          .then(response => response.json())
          .then(data => {
                this.setState({
                    apiData: data
                });
            });
    }
    constructor(props) {
        super(props);
        this.state = {
            apiData: null
        };
    }
    render() {
        return (
            <div>
                {this.state.apiData? (
                    <p>{JSON.stringify(this.state.apiData)}</p>
                ) : (
                    <p>Loading...</p>
                )}
            </div>
        );
    }
}

export default MyComponent;
- **注意事项**:由于它在 DOM 挂载后调用,所以可以安全地访问 DOM 元素。但要注意,如果在此方法中发起网络请求,要处理好错误情况,避免请求失败导致组件处于未定义状态。同时,如果有多个组件在同一页面挂载,要注意它们的挂载顺序可能会影响一些依赖于 DOM 结构的操作。

更新阶段

  1. static getDerivedStateFromProps(props, state):在更新阶段,这个方法同样会被调用,其作用和挂载阶段一样,用于根据新的 props 更新 state。例如,当父组件传递新的 props 给子组件,而子组件需要根据这些新 props 更新自己的 state 时,可以在这个方法中实现。
  2. shouldComponentUpdate(nextProps, nextState)
    • 作用:这个方法用于判断组件是否需要更新。它接收两个参数,nextProps 和 nextState,分别表示即将更新的 props 和 state。返回 true 表示组件需要更新,返回 false 则表示不需要更新,React 会跳过后续的更新流程,直接复用之前的 DOM 树。
    • 代码示例
import React, { Component } from'react';

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }
    shouldComponentUpdate(nextProps, nextState) {
        // 只有当 count 变化时才更新
        return nextState.count!== this.state.count;
    }
    handleClick() {
        this.setState({
            count: this.state.count + 1
        });
    }
    render() {
        return (
            <div>
                <p>Count: {this.state.count}</p>
                <button onClick={this.handleClick}>Increment</button>
            </div>
        );
    }
}

export default MyComponent;
- **注意事项**:此方法返回值的判断逻辑要谨慎编写。过于简单的判断可能导致组件不能及时更新,而过于复杂的判断可能影响性能。同时,要注意不要在此方法中修改 state 或 props,因为它只用于决定是否更新,不应该产生副作用。

3. render():在更新阶段,render 方法会再次被调用,根据最新的 props 和 state 重新生成 React 元素,React 会通过对比新旧元素来决定如何高效地更新 DOM。 4. getSnapshotBeforeUpdate(prevProps, prevState) - 作用:在最近一次渲染输出(提交到 DOM 节点)之前调用。它可以用来在组件更新之前捕获一些信息,比如滚动位置等。返回的值会作为参数传递给 componentDidUpdate 方法。 - 代码示例

import React, { Component } from'react';

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            items: [],
            newItem: ''
        };
    }
    handleChange = (e) => {
        this.setState({
            newItem: e.target.value
        });
    }
    handleSubmit = (e) => {
        e.preventDefault();
        this.setState((prevState) => {
            return {
                items: [...prevState.items, prevState.newItem],
                newItem: ''
            };
        });
    }
    getSnapshotBeforeUpdate(prevProps, prevState) {
        if (prevState.items.length!== this.state.items.length) {
            const list = this.listRef.current;
            return list.scrollHeight - list.clientHeight;
        }
        return null;
    }
    componentDidUpdate(prevProps, prevState, snapshot) {
        if (snapshot!== null) {
            const list = this.listRef.current;
            list.scrollTop = snapshot;
        }
    }
    render() {
        return (
            <div>
                <form onSubmit={this.handleSubmit}>
                    <input
                        type="text"
                        value={this.state.newItem}
                        onChange={this.handleChange}
                    />
                    <button type="submit">Add Item</button>
                </form>
                <ul
                    ref={(ref) => { this.listRef = ref; }}
                >
                    {this.state.items.map((item, index) => (
                        <li key={index}>{item}</li>
                    ))}
                </ul>
            </div>
        );
    }
}

export default MyComponent;
- **注意事项**:此方法返回的值通常用于在更新后恢复组件的某些状态,如滚动位置。它必须在 render 之后立即调用,所以不能在里面执行导致副作用的操作,如修改 DOM 等。

5. componentDidUpdate(prevProps, prevState, snapshot) - 作用:在组件更新后调用。它接收更新前的 props、state 以及 getSnapshotBeforeUpdate 返回的值(如果有)。这个方法通常用于执行依赖于 DOM 更新的操作,如操作更新后的 DOM、根据新的 props 或 state 发起网络请求等。 - 代码示例

import React, { Component } from'react';

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

export default MyComponent;
- **注意事项**:由于它在 DOM 更新后调用,所以可以安全地操作更新后的 DOM。但要注意避免在此方法中进行不必要的更新操作,导致无限循环。如果在此方法中发起网络请求,要注意请求频率,避免过度请求影响性能。

卸载阶段

  1. componentWillUnmount()
    • 作用:在组件从 DOM 中移除之前调用。这个方法通常用于清理资源,如取消定时器、取消网络请求、解绑事件监听器等。
    • 代码示例
import React, { Component } from'react';

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            timer: null
        };
    }
    componentDidMount() {
        const interval = setInterval(() => {
            console.log('Timer is running');
        }, 1000);
        this.setState({
            timer: interval
        });
    }
    componentWillUnmount() {
        clearInterval(this.state.timer);
        console.log('Component is being unmounted, timer cleared');
    }
    render() {
        return <div>Component with a timer</div>;
    }
}

export default MyComponent;
- **注意事项**:在这个方法中执行清理操作是非常重要的,否则可能会导致内存泄漏等问题。比如如果不取消定时器,即使组件已经从 DOM 中移除,定时器仍然会在后台运行,消耗资源。同时,要注意在这个方法中不要调用 setState,因为组件即将被卸载,不会再进行渲染。

React 生命周期的变化与优化

随着 React 的发展,生命周期方法也有所变化。在 React 16.3 引入了新的生命周期方法,同时弃用了一些旧的方法,如 componentWillMount、componentWillUpdate 和 componentWillReceiveProps。这些旧方法在异步渲染模式下可能会导致一些问题,如重复触发、state 更新不一致等。

为什么要进行这些变化

React 引入 Fiber 架构后,渲染过程变为可中断的异步操作。旧的生命周期方法在这种异步环境下可能会出现一些不可预测的行为。例如,componentWillMount 可能会在组件挂载之前多次调用,因为 React 可能会在不同时间点尝试挂载组件。新的生命周期方法则是为了适应 Fiber 架构,确保在异步渲染过程中组件的行为更加可控和可预测。

如何利用新生命周期方法进行优化

  1. 合理使用 static getDerivedStateFromProps 和 componentDidUpdate:通过在 static getDerivedStateFromProps 中根据 props 更新 state,并且在 componentDidUpdate 中处理副作用操作,可以使组件逻辑更加清晰和可控。例如,在处理根据 props 变化而更新 state 的场景下,使用 static getDerivedStateFromProps 可以避免在 componentDidUpdate 中进行复杂的条件判断,同时保证 state 的更新不会触发额外的不必要渲染。
  2. 利用 shouldComponentUpdate 进行性能优化:精确地编写 shouldComponentUpdate 的返回逻辑,可以有效地减少不必要的渲染。例如,在一个列表组件中,如果列表项的 props 没有变化,通过 shouldComponentUpdate 返回 false 可以避免整个列表的重新渲染,大大提高性能。
  3. 在 componentDidMount 和 componentWillUnmount 中管理资源:确保在 componentDidMount 中正确初始化资源,如网络请求、定时器等,并在 componentWillUnmount 中及时清理这些资源,防止内存泄漏和不必要的资源消耗。

总结 React 生命周期方法的使用场景

  1. 数据获取:在 componentDidMount 中发起网络请求获取数据,因为此时组件已经挂载到 DOM,并且可以在 componentDidUpdate 中根据 props 的变化重新获取数据。
  2. DOM 操作:在 componentDidMount 和 componentDidUpdate 中进行 DOM 操作,如初始化第三方库、操作更新后的 DOM 结构等。
  3. 资源管理:在 componentDidMount 中初始化资源,如定时器、事件监听器等,并在 componentWillUnmount 中清理这些资源。
  4. 性能优化:通过 shouldComponentUpdate 方法控制组件的更新,避免不必要的渲染,提高性能。

案例分析:一个复杂表单组件的生命周期应用

假设我们要开发一个复杂的表单组件,该组件需要在挂载时获取初始数据,在用户输入时实时验证,并且在卸载时清理一些临时资源。

  1. 在 constructor 中初始化 state 和绑定方法
constructor(props) {
    super(props);
    this.state = {
        formData: {
            username: '',
            password: ''
        },
        errors: {}
    };
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
}
  1. 在 componentDidMount 中获取初始数据
componentDidMount() {
    fetch('https://example.com/api/form/initialData')
      .then(response => response.json())
      .then(data => {
            this.setState({
                formData: {
                    username: data.username,
                    password: data.password
                }
            });
        });
}
  1. 在 handleChange 中处理用户输入并验证
handleChange(e) {
    const { name, value } = e.target;
    this.setState((prevState) => {
        const newFormData = {
          ...prevState.formData,
            [name]: value
        };
        const newErrors = {
          ...prevState.errors
        };
        if (name === 'username' && value.length < 3) {
            newErrors.username = 'Username must be at least 3 characters';
        } else {
            delete newErrors.username;
        }
        if (name === 'password' && value.length < 6) {
            newErrors.password = 'Password must be at least 6 characters';
        } else {
            delete newErrors.password;
        }
        return {
            formData: newFormData,
            errors: newErrors
        };
    });
}
  1. 在 shouldComponentUpdate 中控制更新
shouldComponentUpdate(nextProps, nextState) {
    return (
        JSON.stringify(nextState.formData)!== JSON.stringify(this.state.formData) ||
        JSON.stringify(nextState.errors)!== JSON.stringify(this.state.errors)
    );
}
  1. 在 componentWillUnmount 中清理临时资源
componentWillUnmount() {
    // 假设这里有一些临时缓存数据需要清理
    localStorage.removeItem('formTempData');
}

通过这样的方式,我们可以全面地利用 React 生命周期方法来构建一个功能完备且性能良好的复杂表单组件。在实际开发中,不同的组件根据其功能需求,灵活运用这些生命周期方法,可以有效地提高开发效率和应用性能。同时,随着 React 的不断发展,开发者需要关注生命周期方法的变化,及时调整代码,以适应新的架构和特性。