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

React Hooks与类组件的生命周期对比

2022-07-251.2k 阅读

React 类组件的生命周期

1. 挂载阶段

  • constructor: 这是类组件特有的方法,在创建组件实例时最先被调用。主要用于初始化 state 以及绑定方法。例如:
import React, { Component } from 'react';

class ExampleComponent 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>
        );
    }
}

这里在 constructor 中初始化了 count 状态,并将 handleClick 方法绑定到组件实例上,确保在方法中 this 指向正确。

  • componentWillMount: 在组件即将插入 DOM 之前调用。这个方法在 React v16.3 之后被标记为过时,不推荐使用。在这个方法中可以执行一些异步操作,比如数据获取,但通常现在我们会将这类操作放在 componentDidMount 中。例如:
import React, { Component } from 'react';

class ExampleComponent extends Component {
    componentWillMount() {
        console.log('Component will mount');
    }

    render() {
        return <div>Example Component</div>;
    }
}
  • render: 这是组件中唯一必需的方法。它返回 React 元素,描述了组件在屏幕上应该呈现的样子。它是纯函数,不能在里面修改 state 或执行副作用操作。例如:
import React, { Component } from 'react';

class ExampleComponent extends Component {
    render() {
        return <div>Hello, React!</div>;
    }
}
  • componentDidMount: 在组件被插入到 DOM 后立即调用。这是执行副作用操作的好地方,比如数据获取、订阅事件、初始化第三方库等。例如,使用 fetch 进行数据获取:
import React, { Component } from 'react';

class ExampleComponent 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>
        );
    }
}

2. 更新阶段

  • componentWillReceiveProps(nextProps): 在组件接收到新的 props 时被调用,但初始化时不会调用。通过比较 nextPropsthis.props 可以决定是否更新 state。不过从 React v16.3 开始,这个方法被标记为过时,不推荐使用。例如:
import React, { Component } from 'react';

class ExampleComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            derivedData: this.props.initialValue
        };
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.initialValue!== this.props.initialValue) {
            this.setState({
                derivedData: nextProps.initialValue
            });
        }
    }

    render() {
        return <div>{this.state.derivedData}</div>;
    }
}
  • shouldComponentUpdate(nextProps, nextState): 在接收到新的 props 或 state 时调用,返回一个布尔值,决定组件是否应该更新。返回 true 表示更新,false 表示不更新。这是一个性能优化的重要方法,通过比较新旧 props 和 state 来避免不必要的渲染。例如:
import React, { Component } from 'react';

class ExampleComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }

    shouldComponentUpdate(nextProps, nextState) {
        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>
        );
    }
}
  • componentWillUpdate(nextProps, nextState): 在组件即将因为 props 或 state 的变化而更新时调用。在 React v16.3 之后被标记为过时,不推荐使用。不能在这个方法中调用 setState。例如:
import React, { Component } from 'react';

class ExampleComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }

    componentWillUpdate(nextProps, nextState) {
        console.log('Component will update with new state:', nextState);
    }

    handleClick() {
        this.setState({
            count: this.state.count + 1
        });
    }

    render() {
        return (
            <div>
                <p>Count: {this.state.count}</p>
                <button onClick={this.handleClick}>Increment</button>
            </div>
        );
    }
}
  • render: 在更新阶段同样会调用 render 方法,根据新的 props 和 state 生成新的 React 元素。
  • componentDidUpdate(prevProps, prevState): 在组件更新后调用。可以在此处执行一些需要在更新后进行的操作,比如操作 DOM 或根据新的 props 或 state 进行数据同步。例如:
import React, { Component } from 'react';

class ExampleComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }

    componentDidUpdate(prevProps, prevState) {
        if (this.state.count!== prevState.count) {
            console.log('Count has been updated:', 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>
        );
    }
}

3. 卸载阶段

  • componentWillUnmount: 在组件从 DOM 中移除之前调用。可以在这里执行清理操作,比如取消订阅事件、清除定时器等。例如:
import React, { Component } from 'react';

class ExampleComponent extends Component {
    constructor(props) {
        super(props);
        this.timer = null;
    }

    componentDidMount() {
        this.timer = setInterval(() => {
            console.log('Timer is running');
        }, 1000);
    }

    componentWillUnmount() {
        clearInterval(this.timer);
        console.log('Component is being unmounted, timer cleared');
    }

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

React Hooks 与生命周期的对应关系

1. 挂载与卸载

  • useEffect 模拟 componentDidMount 和 componentWillUnmountuseEffect 是 React Hooks 中用于处理副作用的 Hook。当传递一个空数组作为第二个参数时,useEffect 回调函数在组件挂载后只执行一次,类似于 componentDidMount。同时,useEffect 返回的清理函数会在组件卸载时执行,类似于 componentWillUnmount。例如:
import React, { useEffect } from'react';

const ExampleComponent = () => {
    useEffect(() => {
        console.log('Component mounted');
        return () => {
            console.log('Component unmounted');
        };
    }, []);

    return <div>Example Component</div>;
};

在这个例子中,useEffect 回调函数中的 console.log('Component mounted') 会在组件挂载时执行,而返回的清理函数 console.log('Component unmounted') 会在组件卸载时执行。

  • 使用场景对比: 在类组件中,componentDidMountcomponentWillUnmount 是分开的方法,逻辑分散在不同的生命周期方法中。而在 Hooks 中,通过 useEffect 及其返回的清理函数,将挂载和卸载相关的逻辑集中在一个地方,使代码结构更紧凑。例如,在类组件中初始化和清理 WebSocket 连接:
import React, { Component } from'react';

class WebSocketComponent extends Component {
    constructor(props) {
        super(props);
        this.socket = null;
    }

    componentDidMount() {
        this.socket = new WebSocket('ws://example.com');
        this.socket.onopen = () => {
            console.log('WebSocket connected');
        };
    }

    componentWillUnmount() {
        this.socket.close();
        console.log('WebSocket disconnected');
    }

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

在 Hooks 中实现相同功能:

import React, { useEffect } from'react';

const WebSocketComponent = () => {
    useEffect(() => {
        const socket = new WebSocket('ws://example.com');
        socket.onopen = () => {
            console.log('WebSocket connected');
        };
        return () => {
            socket.close();
            console.log('WebSocket disconnected');
        };
    }, []);

    return <div>WebSocket Component</div>;
};

可以看到,Hooks 方式将连接和断开连接的逻辑写在一起,更便于理解和维护。

2. 更新

  • useEffect 模拟 componentDidUpdate: 当 useEffect 的第二个参数数组中包含某些值时,只有这些值发生变化时,useEffect 回调函数才会执行,这可以模拟 componentDidUpdate 的功能。例如,当组件的某个 prop 变化时执行副作用操作:
import React, { useEffect } from'react';

const ExampleComponent = ({ value }) => {
    useEffect(() => {
        console.log('Value has changed:', value);
    }, [value]);

    return <div>{value}</div>;
};

在这个例子中,只有当 value prop 发生变化时,useEffect 回调函数才会执行,打印出 Value has changed: [new value]

  • 对比类组件的 componentDidUpdate: 在类组件中,componentDidUpdate 需要手动比较 prevPropsthis.props 来决定是否执行某些操作。而在 Hooks 中,通过 useEffect 的依赖数组可以更简洁地实现同样的功能。例如,在类组件中监听 prop 的变化:
import React, { Component } from'react';

class ExampleComponent extends Component {
    componentDidUpdate(prevProps) {
        if (prevProps.value!== this.props.value) {
            console.log('Value has changed:', this.props.value);
        }
    }

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

Hooks 方式则更加直观和简洁,通过依赖数组明确指定了需要监听的变量。

3. 数据获取

  • 类组件中的数据获取: 在类组件中,通常在 componentDidMount 中进行数据获取。例如:
import React, { Component } from'react';

class DataFetchingComponent 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>
        );
    }
}
  • Hooks 中的数据获取: 在 Hooks 中,可以使用 useEffect 来进行数据获取。例如:
import React, { useEffect, useState } from'react';

const DataFetchingComponent = () => {
    const [data, setData] = useState(null);
    useEffect(() => {
        fetch('https://example.com/api/data')
          .then(response => response.json())
          .then(data => setData(data));
    }, []);

    return (
        <div>
            {data? <p>{JSON.stringify(data)}</p> : <p>Loading...</p>}
        </div>
    );
};

虽然两者都能实现数据获取的功能,但 Hooks 方式利用 useStateuseEffect 使代码更加简洁和直观。useState 用于管理状态,useEffect 用于处理副作用,将数据获取逻辑与状态管理紧密结合。

4. 性能优化

  • 类组件的 shouldComponentUpdate: 在类组件中,shouldComponentUpdate 用于控制组件是否需要更新,通过手动比较 nextPropsthis.props 以及 nextStatethis.state 来决定返回 truefalse。例如:
import React, { Component } from'react';

class ExampleComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }

    shouldComponentUpdate(nextProps, nextState) {
        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>
        );
    }
}
  • Hooks 的 React.memo 和 useMemo/useCallback: 在 Hooks 中,React.memo 用于对函数组件进行性能优化,类似于类组件的 shouldComponentUpdate。它会浅比较 props,如果 props 没有变化,组件不会重新渲染。例如:
import React from'react';

const ExampleComponent = React.memo(({ value }) => {
    return <div>{value}</div>;
});

useMemo 用于缓存计算结果,只有当依赖项变化时才重新计算。例如:

import React, { useMemo } from'react';

const ExampleComponent = ({ a, b }) => {
    const result = useMemo(() => a + b, [a, b]);
    return <div>{result}</div>;
};

useCallback 用于缓存函数,只有当依赖项变化时才重新创建函数,常用于将函数作为 prop 传递给子组件,避免不必要的渲染。例如:

import React, { useCallback } from'react';

const ParentComponent = () => {
    const [count, setCount] = React.useState(0);
    const handleClick = useCallback(() => {
        setCount(count + 1);
    }, [count]);

    return (
        <div>
            <ChildComponent onClick={handleClick} />
            <p>Count: {count}</p>
        </div>
    );
};

const ChildComponent = React.memo(({ onClick }) => {
    return <button onClick={onClick}>Increment</button>;
});

通过这些方式,Hooks 提供了与类组件类似但更加灵活和简洁的性能优化手段。

React Hooks 相对于类组件生命周期的优势

1. 代码结构更简洁

  • 逻辑集中: 在类组件中,生命周期方法将不同阶段的逻辑分散开来。例如,数据获取在 componentDidMount,清理在 componentWillUnmount。而在 Hooks 中,useEffect 可以将相关的逻辑集中在一个地方。以一个订阅事件并在组件卸载时取消订阅的场景为例: 在类组件中:
import React, { Component } from'react';

class EventSubscriptionComponent extends Component {
    constructor(props) {
        super(props);
        this.eventHandler = this.eventHandler.bind(this);
    }

    componentDidMount() {
        window.addEventListener('scroll', this.eventHandler);
    }

    componentWillUnmount() {
        window.removeEventListener('scroll', this.eventHandler);
    }

    eventHandler() {
        console.log('Window scrolled');
    }

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

在 Hooks 中:

import React, { useEffect } from'react';

const EventSubscriptionComponent = () => {
    useEffect(() => {
        const eventHandler = () => {
            console.log('Window scrolled');
        };
        window.addEventListener('scroll', eventHandler);
        return () => {
            window.removeEventListener('scroll', eventHandler);
        };
    }, []);

    return <div>Event Subscription Component</div>;
};

可以看到,Hooks 将事件监听和取消监听的逻辑写在一起,代码结构更清晰,更易于理解和维护。

  • 减少样板代码: 类组件需要继承 Component,定义 constructor 来初始化 state 和绑定方法等,存在较多样板代码。而 Hooks 函数组件简洁明了,不需要这些繁琐的步骤。例如:
// 类组件
import React, { Component } from'react';

class CounterComponent 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>
        );
    }
}
// Hooks 组件
import React, { useState } from'react';

const CounterComponent = () => {
    const [count, setCount] = useState(0);
    const handleClick = () => {
        setCount(count + 1);
    };

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={handleClick}>Increment</button>
        </div>
    );
};

Hooks 组件省略了 constructorsuper 和手动绑定方法等步骤,代码量明显减少。

2. 状态逻辑复用更容易

  • 高阶组件和 Render Props 的问题: 在类组件中,复用状态逻辑通常使用高阶组件(HOC)或 Render Props 模式。但这两种方式会导致组件嵌套过深,增加代码的复杂性和调试难度。例如,使用高阶组件来添加日志功能:
import React, { Component } from'react';

const withLogging = (WrappedComponent) => {
    return class extends Component {
        componentDidMount() {
            console.log('Component mounted:', this.props);
        }

        componentDidUpdate(prevProps) {
            console.log('Component updated:', this.props, 'prevProps:', prevProps);
        }

        componentWillUnmount() {
            console.log('Component unmounted');
        }

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

class ExampleComponent extends Component {
    render() {
        return <div>Example Component</div>;
    }
}

const LoggedExampleComponent = withLogging(ExampleComponent);
  • 自定义 Hooks 解决复用问题: 在 Hooks 中,可以通过创建自定义 Hooks 来复用状态逻辑。自定义 Hooks 是一个函数,其名称以 use 开头,在函数内部可以调用其他 Hooks。例如,创建一个自定义 Hooks 来复用数据获取逻辑:
import React, { useEffect, useState } from'react';

const useDataFetching = (url) => {
    const [data, setData] = useState(null);
    useEffect(() => {
        fetch(url)
          .then(response => response.json())
          .then(data => setData(data));
    }, [url]);
    return data;
};

const Component1 = () => {
    const data = useDataFetching('https://example.com/api/data1');
    return (
        <div>
            {data? <p>{JSON.stringify(data)}</p> : <p>Loading...</p>}
        </div>
    );
};

const Component2 = () => {
    const data = useDataFetching('https://example.com/api/data2');
    return (
        <div>
            {data? <p>{JSON.stringify(data)}</p> : <p>Loading...</p>}
        </div>
    );
};

自定义 Hooks 使得状态逻辑复用更加简单直接,避免了高阶组件和 Render Props 带来的嵌套问题。

3. 更好的 TypeScript 支持

  • 类组件的 TypeScript 问题: 在类组件中使用 TypeScript 时,需要处理复杂的类型定义。例如,定义 props 和 state 的类型,以及生命周期方法的类型。对于 shouldComponentUpdate 方法,需要精确比较 nextPropsthis.props 的类型,这增加了开发的难度和代码的复杂性。
import React, { Component } from'react';

interface ExampleProps {
    value: number;
}

interface ExampleState {
    count: number;
}

class ExampleComponent extends Component<ExampleProps, ExampleState> {
    constructor(props: ExampleProps) {
        super(props);
        this.state = {
            count: 0
        };
    }

    shouldComponentUpdate(nextProps: ExampleProps, nextState: ExampleState): boolean {
        return nextState.count!== this.state.count || nextProps.value!== this.props.value;
    }

    render() {
        return <div>{this.state.count}</div>;
    }
}
  • Hooks 的 TypeScript 优势: 在 Hooks 中,TypeScript 的类型推断更加自然和简洁。例如,使用 useStateuseEffect 时,TypeScript 可以自动推断类型。对于自定义 Hooks,也更容易定义类型。
import React, { useState, useEffect } from'react';

const ExampleComponent = () => {
    const [count, setCount] = useState(0);
    const [value, setValue] = useState('');

    useEffect(() => {
        console.log('Count or value has changed:', count, value);
    }, [count, value]);

    return (
        <div>
            <p>Count: {count}</p>
            <input value={value} onChange={(e) => setValue(e.target.value)} />
        </div>
    );
};

Hooks 与 TypeScript 的结合使得代码更加简洁,类型定义更加清晰,减少了类型相关的错误。

React Hooks 相对于类组件生命周期的劣势

1. 学习曲线

  • 概念的转变: 对于习惯了类组件开发的开发者来说,Hooks 带来了全新的概念和思维方式。从基于类的编程转变为函数式编程,需要理解诸如 useStateuseEffect 等 Hooks 的工作原理,以及如何在函数组件中处理副作用和状态管理。例如,在类组件中,状态是通过 this.state 来访问和更新,而在 Hooks 中则是通过 useState 返回的数组解构来实现。
// 类组件
import React, { Component } from'react';

class ExampleComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }

    handleClick() {
        this.setState({
            count: this.state.count + 1
        });
    }

    render() {
        return (
            <div>
                <p>Count: {this.state.count}</p>
                <button onClick={this.handleClick}>Increment</button>
            </div>
        );
    }
}
// Hooks 组件
import React, { useState } from'react';

const ExampleComponent = () => {
    const [count, setCount] = useState(0);
    const handleClick = () => {
        setCount(count + 1);
    };

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={handleClick}>Increment</button>
        </div>
    );
};

这种概念上的转变可能需要一定的时间来适应,对于初学者来说增加了学习的难度。

  • 规则的复杂性: Hooks 有严格的使用规则,例如只能在函数组件的顶层调用 Hooks,不能在循环、条件语句或嵌套函数中调用。这些规则需要开发者牢记,否则会导致难以调试的错误。例如:
import React, { useState } from'react';

const ExampleComponent = () => {
    const condition = true;
    if (condition) {
        const [count, setCount] = useState(0); // 错误:不能在条件语句中调用 Hooks
        return <div>{count}</div>;
    }
    return null;
};

相比之下,类组件的生命周期方法调用规则相对简单,开发者只需要在合适的生命周期阶段编写相应的逻辑即可。

2. 调试难度

  • 缺少实例和原型链: 在类组件中,由于有组件实例和原型链,调试时可以通过 this 来访问组件的状态和方法,更容易追踪问题。例如,在 componentDidUpdate 中设置断点,可以查看 this.propsthis.state 的值,分析更新的原因。
import React, { Component } from'react';

class ExampleComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }

    componentDidUpdate(prevProps, prevState) {
        debugger;
        if (prevState.count!== this.state.count) {
            console.log('Count has been updated:', 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>
        );
    }
}

而在 Hooks 中,函数组件没有 this 指向,调试时需要通过其他方式来追踪状态变化。例如,使用 React DevTools 来查看状态和副作用的执行情况,但这可能不如在类组件中直接通过 this 访问直观。

  • 副作用调试: 在 Hooks 中,useEffect 用于处理副作用,但如果副作用逻辑复杂,例如包含多个异步操作或依赖关系,调试起来会比较困难。由于 useEffect 的执行依赖于依赖数组,不正确的依赖数组设置可能导致副作用执行异常,而排查这种问题相对繁琐。例如:
import React, { useEffect } from'react';

const ExampleComponent = () => {
    const [data, setData] = useState(null);
    useEffect(() => {
        const fetchData = async () => {
            const response1 = await fetch('https://example.com/api/data1');
            const response2 = await fetch('https://example.com/api/data2');
            const result1 = await response1.json();
            const result2 = await response2.json();
            setData({...result1,...result2 });
        };
        fetchData();
    }, []);

    return <div>{data? <p>{JSON.stringify(data)}</p> : <p>Loading...</p>}</div>;
};

如果数据没有正确获取或合并,调试时需要仔细检查 useEffect 的依赖数组以及异步操作的逻辑,这增加了调试的难度。

3. 兼容性和生态系统

  • 旧项目升级: 对于已经使用类组件构建的大型项目,升级到 Hooks 可能面临较大的成本。需要逐个将类组件转换为函数组件并使用 Hooks,这涉及到大量的代码修改和测试工作。同时,在升级过程中可能会引入新的错误,影响项目的稳定性。例如,一个包含数百个类组件的项目,升级到 Hooks 可能需要投入大量的人力和时间进行代码重构和测试。
  • 第三方库支持: 虽然现在大多数主流的第三方库都已经开始支持 Hooks,但仍有一些旧的库可能只支持类组件。在使用这些库时,可能需要使用高阶组件或其他方式来适配 Hooks,增加了开发的复杂性。例如,某些 UI 库可能提供的 API 是基于类组件的生命周期设计的,在 Hooks 组件中使用时可能需要额外的封装和适配。