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

React 使用 useEffect 替代类组件生命周期方法

2021-01-173.0k 阅读

React 类组件生命周期方法回顾

在 React 类组件中,生命周期方法是至关重要的一部分,它们允许开发者在组件的不同阶段执行特定的操作。

挂载阶段

  • constructor:组件的构造函数,在创建组件实例时被调用。通常用于初始化 state 和绑定方法。例如:
class MyComponent extends React.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>
    );
  }
}
  • componentDidMount:在组件被插入到 DOM 后立即调用。常用于需要 DOM 节点的操作,或者发起网络请求等。比如:
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: []
    };
  }
  componentDidMount() {
    fetch('https://example.com/api/data')
    .then(response => response.json())
    .then(data => this.setState({ data }));
  }
  render() {
    return (
      <div>
        {this.state.data.map(item => (
          <p key={item.id}>{item.name}</p>
        ))}
      </div>
    );
  }
}

更新阶段

  • shouldComponentUpdate:在组件接收到新的 props 或 state 时被调用,返回一个布尔值,决定组件是否应该更新。这可以用于性能优化,避免不必要的重新渲染。例如:
class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.value!== nextProps.value) {
      return true;
    }
    return false;
  }
  render() {
    return <div>{this.props.value}</div>;
  }
}
  • componentDidUpdate:在组件更新后被调用。可以在此处进行依赖于 DOM 更新的操作,或者比较更新前后的 props 和 state。例如:
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: this.props.initialValue
    };
  }
  componentDidUpdate(prevProps, prevState) {
    if (prevProps.initialValue!== this.props.initialValue) {
      this.setState({
        value: this.props.initialValue
      });
    }
  }
  render() {
    return <div>{this.state.value}</div>;
  }
}

卸载阶段

  • componentWillUnmount:在组件从 DOM 中移除之前被调用。常用于清理副作用,如取消网络请求、清除定时器等。例如:
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.timer = null;
  }
  componentDidMount() {
    this.timer = setInterval(() => {
      console.log('Component is running');
    }, 1000);
  }
  componentWillUnmount() {
    clearInterval(this.timer);
  }
  render() {
    return <div>My Component</div>;
  }
}

useEffect 基础

React 的 useEffect 钩子函数是在函数组件中处理副作用的主要方式。它的设计理念是将副作用逻辑与渲染逻辑分离,使得代码更加清晰和易于维护。

useEffect 基本语法

useEffect 接收两个参数:一个是包含副作用逻辑的回调函数,另一个是可选的依赖数组。语法如下:

import React, { useEffect } from'react';

function MyComponent() {
  useEffect(() => {
    // 副作用逻辑
    console.log('Component mounted or updated');

    return () => {
      // 清理逻辑
      console.log('Component will unmount or dependencies changed');
    };
  }, []);

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

在上述代码中,useEffect 的回调函数在组件挂载后和每次更新后都会执行。返回的函数(如果有的话)会在组件卸载或依赖数组中的值发生变化时执行,用于清理副作用。

依赖数组的作用

依赖数组决定了 useEffect 回调函数的触发时机。当依赖数组为空时,useEffect 回调函数只会在组件挂载后执行一次,类似于 componentDidMount。例如:

import React, { useEffect } from'react';

function MyComponent() {
  useEffect(() => {
    console.log('Component mounted');

    return () => {
      console.log('Component will unmount');
    };
  }, []);

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

当依赖数组包含某些值时,useEffect 回调函数会在这些值发生变化时执行。例如:

import React, { useEffect, useState } from'react';

function MyComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(`Count changed to ${count}`);

    return () => {
      console.log(`Count is about to change from ${count}`);
    };
  }, [count]);

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

在这个例子中,useEffect 会在 count 状态改变时触发,模拟了 componentDidUpdate 中依赖于特定 state 变化的行为。

使用 useEffect 替代挂载阶段生命周期方法

替代 constructor 初始化 state

在函数组件中,我们使用 useState 钩子来初始化和更新 state,而不是在 constructor 中。例如,之前类组件的 constructor 初始化 count state:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  render() {
    return <div>{this.state.count}</div>;
  }
}

在函数组件中使用 useState

import React, { useState } from'react';

function MyComponent() {
  const [count, setCount] = useState(0);
  return <div>{count}</div>;
}

替代 componentDidMount

useEffect 配合空的依赖数组可以完全替代 componentDidMount。比如之前在 componentDidMount 中发起网络请求:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: []
    };
  }
  componentDidMount() {
    fetch('https://example.com/api/data')
    .then(response => response.json())
    .then(data => this.setState({ data }));
  }
  render() {
    return (
      <div>
        {this.state.data.map(item => (
          <p key={item.id}>{item.name}</p>
        ))}
      </div>
    );
  }
}

在函数组件中使用 useEffect

import React, { useEffect, useState } from'react';

function MyComponent() {
  const [data, setData] = useState([]);

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

  return (
    <div>
      {data.map(item => (
        <p key={item.id}>{item.name}</p>
      ))}
    </div>
  );
}

使用 useEffect 替代更新阶段生命周期方法

替代 shouldComponentUpdate

虽然 useEffect 本身不能直接替代 shouldComponentUpdate,但 React.memo 可以用于函数组件的性能优化,类似于 shouldComponentUpdate 的功能。React.memo 是一个高阶组件,它会对函数组件的 props 进行浅比较,如果 props 没有变化,就不会重新渲染组件。例如:

function MyComponent(props) {
  return <div>{props.value}</div>;
}

export default React.memo(MyComponent);

在上述代码中,MyComponent 只有在 props.value 发生变化时才会重新渲染,实现了类似 shouldComponentUpdate 的功能。

替代 componentDidUpdate

useEffect 可以通过设置依赖数组来模拟 componentDidUpdate 的行为。例如,之前在 componentDidUpdate 中比较 props 的变化:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: this.props.initialValue
    };
  }
  componentDidUpdate(prevProps, prevState) {
    if (prevProps.initialValue!== this.props.initialValue) {
      this.setState({
        value: this.props.initialValue
      });
    }
  }
  render() {
    return <div>{this.state.value}</div>;
  }
}

在函数组件中使用 useEffect

import React, { useEffect, useState } from'react';

function MyComponent({ initialValue }) {
  const [value, setValue] = useState(initialValue);

  useEffect(() => {
    setValue(initialValue);
  }, [initialValue]);

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

在这个例子中,useEffect 会在 initialValue prop 变化时触发,从而更新 value state,模拟了 componentDidUpdate 中对 props 变化的处理。

使用 useEffect 替代卸载阶段生命周期方法

替代 componentWillUnmount

useEffect 的返回函数可以用于清理副作用,模拟 componentWillUnmount 的行为。例如,之前在 componentWillUnmount 中清除定时器:

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

在函数组件中使用 useEffect

import React, { useEffect } from'react';

function MyComponent() {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Component is running');
    }, 1000);

    return () => {
      clearInterval(timer);
    };
  }, []);

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

在这个例子中,useEffect 的返回函数会在组件卸载时执行,清除定时器,实现了与 componentWillUnmount 相同的功能。

useEffect 的复杂场景处理

多个 useEffect 实现不同逻辑

在实际开发中,一个组件可能有多个不同的副作用逻辑。使用 useEffect 可以轻松实现,每个 useEffect 只负责一种副作用。例如:

import React, { useEffect, useState } from'react';

function MyComponent() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState([]);

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

  useEffect(() => {
    console.log(`Count changed to ${count}`);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ul>
        {data.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

在这个例子中,第一个 useEffect 负责发起网络请求获取数据,第二个 useEffect 负责在 count 变化时打印日志。这种方式使得代码逻辑更加清晰,每个副作用逻辑相互独立。

处理依赖数组的变化

有时候,依赖数组中的值变化可能会导致复杂的逻辑。例如,依赖数组中有多个值,并且不同值的变化需要不同的处理。可以通过在 useEffect 中添加条件判断来处理。例如:

import React, { useEffect, useState } from'react';

function MyComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  useEffect(() => {
    if (count > 0) {
      console.log(`Count is ${count}`);
    }
    if (name.length > 0) {
      console.log(`Name is ${name}`);
    }
  }, [count, name]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter name"
      />
    </div>
  );
}

在这个例子中,useEffect 会在 countname 变化时触发,并且根据不同的值进行不同的处理。

useEffect 的性能优化

避免不必要的依赖

在设置依赖数组时,要确保只包含真正影响副作用逻辑的变量。如果依赖数组中包含了不必要的变量,可能会导致 useEffect 不必要的触发,影响性能。例如:

import React, { useEffect, useState } from'react';

function MyComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  const expensiveCalculation = () => {
    // 复杂的计算逻辑
    return count * name.length;
  };

  useEffect(() => {
    console.log('Effect triggered');
  }, [expensiveCalculation()]); // 错误的依赖设置,每次渲染都会触发

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter name"
      />
    </div>
  );
}

在上述代码中,expensiveCalculation() 每次渲染都会重新计算,导致 useEffect 不必要的触发。应该将依赖改为真正影响副作用的变量,如 countname

import React, { useEffect, useState } from'react';

function MyComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  const expensiveCalculation = () => {
    // 复杂的计算逻辑
    return count * name.length;
  };

  useEffect(() => {
    console.log('Effect triggered');
  }, [count, name]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter name"
      />
    </div>
  );
}

使用 useCallback 和 useMemo 优化依赖

useCallbackuseMemo 可以帮助优化 useEffect 的依赖。useCallback 用于缓存函数,useMemo 用于缓存值。例如:

import React, { useEffect, useState, useCallback, useMemo } from'react';

function MyComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

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

  const expensiveCalculation = useMemo(() => {
    return count * name.length;
  }, [count, name]);

  useEffect(() => {
    console.log('Effect triggered');
  }, [handleClick, expensiveCalculation]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter name"
      />
    </div>
  );
}

在这个例子中,handleClick 函数和 expensiveCalculation 值只会在依赖变化时更新,从而避免了 useEffect 不必要的触发。

总结

通过 useEffect 钩子函数,React 函数组件能够有效地替代类组件的生命周期方法。它提供了一种更加简洁和灵活的方式来处理副作用逻辑,使得代码逻辑更加清晰,易于维护。在使用 useEffect 时,合理设置依赖数组、避免不必要的依赖以及利用 useCallbackuseMemo 进行性能优化是非常重要的。掌握 useEffect 的使用方法,对于编写高效、可维护的 React 应用程序至关重要。无论是简单的组件初始化,还是复杂的副作用处理和性能优化,useEffect 都能提供强大的支持。在实际开发中,根据具体的需求和场景,灵活运用 useEffect,可以让 React 应用的开发更加高效和优雅。同时,随着 React 的不断发展和更新,useEffect 也可能会有更多的特性和优化,开发者需要持续关注和学习,以充分发挥其优势。