【前端面试】如何深度谈class组件和函数组件的区别

React 类组件(Class Components)和函数组件(Function Components)是构建 React 应用程序的两种主要方式。它们在语法、功能和生命周期方面有区别。

基本比较

类组件(Class Components)

  1. 定义方式:使用 ES6 类(class)语法定义。

  2. 状态(State):可以在类组件中使用 this.state 来定义和管理组件的状态。

  3. 生命周期方法:类组件有多个生命周期钩子,如 componentDidMountshouldComponentUpdatecomponentDidUpdatecomponentWillUnmount 等。

  4. 实例方法:可以使用 this 关键字访问组件的属性和方法。

  5. 支持继承:类组件可以继承其他类组件,实现代码复用。

  6. 构造函数:使用构造函数(constructor)来初始化 state 或绑定事件处理函数。

    class MyClassComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = { count: 0 };
      }
    
      componentDidMount() {
        console.log('组件已挂载');
      }
    
      handleClick = () => {
        this.setState({ count: this.state.count + 1 });
      };
    
      render() {
        return <button onClick={this.handleClick}>{this.state.count}</button>;
      }
    }
    

函数组件(Function Components)

  1. 定义方式:使用 JavaScript 函数定义。

  2. 状态(State):在函数组件中,可以使用 useState Hooks 来添加状态。

  3. 生命周期方法:通过使用如 useEffect 的 Hooks 来处理副作用和生命周期逻辑。

  4. 无实例:函数组件没有 this,它们只是返回 JSX。

  5. 无继承:函数组件不支持继承,但可以通过组合或高阶函数来复用逻辑。

  6. Hooks:函数组件可以使用 React 提供的各种 Hooks,如 useStateuseEffectuseContext 等。

    import React, { useState, useEffect } from 'react';
    
    function MyFunctionComponent() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        console.log('组件已挂载');
      }, []); // 空依赖数组相当于 componentDidMount
    
      const handleClick = () => {
        setCount(count + 1);
      };
    
      return <button onClick={handleClick}>{count}</button>;
    }
    

主要区别

  • 语法:类组件使用类语法,而函数组件使用函数语法。
  • 状态管理:类组件通过 this.state 管理状态,函数组件通过 useState Hook。
  • 生命周期:类组件有生命周期方法,函数组件通过 Hooks 如 useEffect 处理生命周期逻辑。
  • 代码复用:类组件通过继承复用代码,函数组件通过组合或自定义 Hooks 复用逻辑。
  • 性能:函数组件通常更轻量,并且更容易被优化,因为它们没有类组件的额外开销。

React 团队推荐在大多数情况下使用函数组件,因为它们更简洁、易于理解,并且具有 Hooks 带来的强大能力。然而,类组件仍然适用于某些特定场景,特别是需要使用生命周期方法或状态和生命周期逻辑较为复杂的组件。随着 React 的发展,许多类组件的功能已经可以通过 Hooks 在函数组件中实现。

React 类组件和函数组件在性能上的差异

React 类组件和函数组件在性能上的差异通常不是很明显,因为 React 的渲染引擎会针对两者进行优化。

实例化开销

  • 类组件需要更多的实例化开销,因为它们需要创建一个类实例,这可能涉及到原型链的查找。
  • this 的引用和可能的闭包,可能会占用更多的内存。

原型方法调用

  • 类组件中的方法调用可能会稍微慢一些,因为它们是通过原型链调用的。

声明式的优化hooks

  • 使用函数组件和 Hooks,例如 React.memouseMemo,可以更容易地避免组件的不必要渲染。
    useMemouseCallback 提供了一种更声明式和自动化的方式来进行这些优化,而类组件则需要手动管理缓存和引用。随着 React 的发展,推荐使用函数组件和 Hooks,因为它们提供了更简洁和易于维护的方式来构建组件。

补充: useMemo
useMemo Hook 用于记忆化复杂计算的结果,确保当依赖项没有变化时,组件不会进行不必要的计算。

函数组件中的使用:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

这里,computeExpensiveValue 函数的执行结果被记忆化,只有当 ab 发生变化时才会重新计算。

类组件中的对应优化:
在类组件中,可以通过缓存计算结果的方式实现类似的优化:

class MyClassComponent extends React.Component {
  componentDidMount() {
    this.cachedValue = this.computeExpensiveValue(this.props.a, this.props.b);
  }

  computeExpensiveValue(a, b) {
    // 复杂计算
  }

  render() {
    // 使用缓存的值
    return <div>{this.cachedValue}</div>;
  }
}

在这个例子中,this.cachedValue 用于存储计算结果,避免在每次渲染时重复计算。

useCallback
useCallback Hook 用于记忆化回调函数,确保回调函数在其依赖项没有变化时保持不变,从而避免不必要的重新渲染。

函数组件中的使用:

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

这里,回调函数的引用在 ab 发生变化时才会更新。

类组件中的对应优化:
在类组件中,可以通过绑定方法或使用 class 字面量属性来确保回调函数的引用不变:

class MyClassComponent extends React.Component {
  handleClick = () => {
    doSomething(this.props.a, this.props.b);
  }

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}

在这个例子中,handleClick 方法在类定义时就绑定了,因此其引用在组件的整个生命周期中保持不变。

错误处理

  • 类组件的错误处理可能涉及到更复杂的栈跟踪,这可能会影响性能。
    错误边界:

补充:React 16 及更高版本支持错误边界(Error Boundaries),这是一种特殊类型的组件,可以捕获其子组件树中任何组件的 JavaScript 错误,并提供回退 UI。

代码拆分

- 函数组件与 React 的懒加载(`React.lazy`)和代码拆分特性更自然地集成。

更新和渲染优化

  • React 团队对函数组件的更新和渲染流程进行了优化,特别是在使用 Hooks 时。

避免不必要的生命周期调用

  • 类组件的生命周期方法(如 componentDidUpdate)可能会在不必要的时候被调用,而函数组件的 useEffect 可以更精确地控制副作用。

现代浏览器优化

- 现代浏览器可能对函数调用有更优化的 JIT(Just-In-Time)编译,这可能使函数组件的性能略优于类组件。

尽管存在这些差异,但在大多数情况下,React 类组件和函数组件的性能差异是可以忽略不计的。React 团队一直在努力优化两者的性能,确保无论选择哪种组件类型,都能获得良好的性能体验。

在实际开发中,选择类组件还是函数组件应基于代码的可维护性、可读性和团队的熟悉度,而不仅仅是性能。随着 React 功能的不断发展,函数组件和 Hooks 提供了一种更现代、更灵活的方式来构建 React 应用程序。

useEffect Hook 模拟类组件

useEffect Hook 在函数组件中非常强大,因为它可以模拟类组件的多个生命周期方法。以下是如何使用 useEffect 来模拟类组件的不同生命周期钩子:

1. componentDidMount / componentDidUpdate

在类组件中,componentDidMount 在组件首次渲染后立即调用,而 componentDidUpdate 在组件更新后调用。在函数组件中,你可以使用一个空依赖数组来模拟这两个钩子:

useEffect(() => {
  // 首次渲染和更新后执行的代码
  console.log('Component mounted or updated');
  // 这里可以执行数据获取、订阅等操作
}, []); // 空依赖数组表示仅在首次渲染时执行

2. componentDidUpdate (带参数)

虽然 useEffect 不直接提供 prevPropsprevState 参数,但你可以通过比较新旧 props 或 state 来模拟:

useEffect(() => {
  if (newProps.id !== prevProps.id) {
    console.log('ID has changed:', newProps.id);
  }
  // 其他 props 更新逻辑...
}, [newProps.id]); // 依赖项数组中的 id 发生变化时执行

3. componentWillUnmount

在类组件中,componentWillUnmount 在组件卸载和销毁前调用。在函数组件中,你可以省略依赖数组来模拟这个钩子:

useEffect(() => {
  console.log('Component will unmount');

  // 清理工作,例如取消订阅或定时器
  return () => {
    console.log('Cleanup on unmount');
  };
}, []); // 没有依赖项,仅在组件卸载时执行

4. componentWillMount (UNSAFE_)

虽然 componentWillMount 已被标记为不安全且不推荐使用,但你仍然可以模拟它的行为:

useEffect(() => {
  console.log('Component will mount (UNSAFE)');

  // 执行 componentWillMount 类似的逻辑
}, []); // 空依赖数组,仅在首次渲染前执行

5. componentWillReceiveProps (UNSAFE_)

同样,componentWillReceiveProps 也已被标记为不安全,但可以通过比较新旧 props 来模拟:

useEffect(() => {
  if (nextProps.someProp !== prevProps.someProp) {
    console.log('someProp has changed');
  }
  // 其他属性更新逻辑...
}, [nextProps.someProp]); // 当 someProp 发生变化时执行

注意事项

  • 避免在 useEffect 中执行阻塞浏览器的长时间运行操作,因为这可能导致性能问题。
  • 确保在 useEffect 的清理函数中清除副作用,如取消网络请求、定时器等。
  • 使用 useEffect 模拟生命周期时,要仔细考虑依赖项数组的内容,以确保副作用按预期执行。

通过合理使用 useEffect,你可以在函数组件中实现与类组件相似的生命周期行为,同时享受 Hooks 带来的其他好处。

深度比较:JavaScript 中的多范式支持

JavaScript 语言本身提供了灵活的特性,使其能够适应不同的编程范式:

面向对象编程:通过构造函数、原型和 class 关键字(ES6+)。
过程式编程:通过循环、条件语句和赋值操作。
函数式编程:通过高阶函数(如 map、filter、reduce)、闭包和不可变性。

上述语言风格谈论的更多是使用react的前端开发者的心智模型。
实际,React 都对两种组件做了各自的优化处理。 从 React 最初的版本开始,无论是函数式组件还是类组件,它们都使用了虚拟 DOM(Virtual DOM)的概念。虚拟 DOM 是 React 的一个核心特性,它允许 React 高效地更新和渲染用户界面。

在React中,无论是函数式组件还是类组件,它们从JSX转换为HTML的阶段变化基本上是一致的。
React在编译时将JSX转换为JavaScript对象,这个过程称为“渲染”。

以下是转换过程的简要概述:

  1. 编写JSX:在React组件中使用JSX编写UI。

  2. 编译JSX:React的编译器(如Babel)将JSX转换为React.createElement()调用。

  3. 创建React元素:React.createElement()函数创建一个JavaScript对象,这个对象描述了UI的结构和属性。

  4. 渲染到DOM:React使用这些React元素来生成实际的DOM节点,并将它们插入到页面中。

  5. 更新DOM:当组件的状态或属性发生变化时,React会重新执行上述过程,并更新DOM以反映新的UI。

对于函数式组件和类组件,这个过程是相同的。主要的区别在于它们是如何定义和实现的:

  • 函数式组件:使用简单的JavaScript函数定义,通常用于表示无状态的UI。它们可以接收props作为参数,并返回JSX。

  • 类组件:使用ES6类定义,通常用于表示有状态的UI。它们拥有生命周期方法和状态管理。

尽管它们的实现方式不同,但它们渲染为HTML的方式是一致的。React的渲染引擎不关心组件是如何定义的,只关心组件返回的JSX。因此,无论是函数式组件还是类组件,它们最终都会被转换为DOM节点,以构建用户界面。

补充:

虚拟 DOM 的基本概念:

  • 虚拟 DOM 树:当使用 React 渲染组件时,React 会创建一个轻量级的虚拟 DOM 树,这是一个 JavaScript 对象,代表了实际 DOM 元素及其属性和子元素。

  • 组件实例化:对于类组件,React 会创建一个组件实例,但对于函数式组件,每次渲染时都会调用函数来生成新的虚拟 DOM。

  • 渲染过程:React 首先在虚拟 DOM 中执行更新,然后使用高效的 diff 算法计算出需要在真实 DOM 上进行的最小更新。

  • 更新 DOM:当组件的状态或属性发生变化时,React 会重新创建一个新的虚拟 DOM 树,并与旧的虚拟 DOM 树进行比较,以确定如何高效地更新 DOM。

函数式组件和虚拟 DOM:

  • 无状态:最初的函数式组件通常没有状态,它们只接收 props 并返回虚拟 DOM 元素。

  • 简洁性:函数式组件的简洁性使得它们在某些情况下更容易理解和使用,尤其是在组件不需要维护状态或生命周期方法时。

  • Hooks:React 16.8 引入的 Hooks API 扩展了函数式组件的能力,使得函数式组件也能够使用状态(useState)、副作用(useEffect)和其他 React 特性。

类组件和虚拟 DOM:

  • 状态和生命周期:类组件可以拥有自己的状态(this.state)和生命周期方法,这些方法在虚拟 DOM 更新和 DOM 操作的不同阶段被调用。

  • 实例化:类组件的实例在组件创建时实例化,并在组件的整个生命周期中保持不变。

  • 更新:当类组件的状态或属性发生变化时,React 会触发重新渲染,实例的 render 方法会被调用,生成新的虚拟 DOM。

总结:

虚拟 DOM 是 React 的一个基础概念,它与组件是函数式还是类没有直接关系。无论是函数式组件还是类组件,它们都通过虚拟 DOM 来描述 UI,React 通过比较新旧虚拟 DOM 树来高效地更新 DOM。随着 React 的发展,函数式组件和 Hooks 成为了构建组件的推荐方式,因为它们提供了更简洁和灵活的代码组织方式。

React Fiber 架构对这两者的优化

React Fiber 架构是 React 16 版本中引入的内部架构,
协调阶段(对比新旧dom)、异步渲染、任务调度、更新排队、副作用的追踪和执行、错误处理、懒加载和代码分割、内存管理、双缓冲技术、渲染中断和恢复。(后续出一篇算法细节~)
Fiber 架构的核心是对 React 的工作单元进行了重新设计,使用链表结构的 Fiber 节点代替了之前的堆栈结构,从而使得 React 能够更细粒度地控制组件的渲染过程。

作为开发者,你通常不需要直接与 Fiber 架构交互。

对 Class 组件的优化:

  1. 错误边界:Fiber 引入了改进的错误边界机制,允许类组件通过静态方法 getDerivedStateFromErrorcomponentDidCatch 捕获子组件中的错误,增强了应用的稳定性。

getDerivedStateFromErrorcomponentDidCatch 是 React 16.3+ 版本中引入的两个生命周期方法,它们用于错误边界(Error Boundaries)机制,允许开发者捕获组件树中抛出的 JavaScript 错误,并提供回退 UI(Fallback UI)。以下是这两个方法的详细说明:

getDerivedStateFromError(静态方法)
  • 用途:这个方法在组件抛出错误时被调用,可以返回一个对象来更新组件的状态,从而触发组件的重新渲染。
  • 使用场景:通常用于更新 UI 以反映错误状态,例如显示错误消息。
  • 参数:接收一个 error 参数,包含被抛出的错误对象。

示例

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, errorMessage: '' };
  }

  static getDerivedStateFromError(error) {
    // 更新状态以触发重新渲染,并显示错误信息
    return { hasError: true, errorMessage: error.message };
  }

  render() {
    if (this.state.hasError) {
      // 渲染回退 UI
      return <h1>{this.state.errorMessage}</h1>;
    }

    return this.props.children;
  }
}
componentDidCatch
  • 用途:这个方法在类组件中捕获到子组件树中的一个错误时被调用。
  • 使用场景:用于记录错误信息、执行副作用(如发送错误报告到服务器)、更新组件状态等。
  • 参数
    • error:包含被抛出的错误对象。
    • errorInfo:一个对象,包含有关错误位置的信息,例如组件堆栈。

示例

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, errorMessage: '' };
  }

  componentDidCatch(error, errorInfo) {
    // 可以在这里记录错误信息
    console.error('Uncaught error:', error, 'Info:', errorInfo);

    // 更新状态以触发重新渲染,并显示错误信息
    this.setState({ hasError: true, errorMessage: error.message });
  }

  render() {
    if (this.state.hasError) {
      // 渲染回退 UI
      return <h1>{this.state.errorMessage}</h1>;
    }

    return this.props.children;
  }
}
共同点和差异
  • 共同点:两者都是错误边界的一部分,用于捕获子组件中的错误,并触发 UI 更新。
  • 差异
    • getDerivedStateFromError 是静态方法,用于更新状态并触发渲染;而 componentDidCatch 是实例方法,除了可以更新状态外,还可以执行其他副作用。
    • getDerivedStateFromError 直接返回一个对象来更新状态;componentDidCatch 通过调用 setState 来更新状态。

使用建议

  • 错误边界应该仅用于捕获渲染过程中的意外错误,不应该用于捕获事件处理程序中的错误。
  • 错误边界不应该滥用,它们应该仅包裹应用中可能抛出错误的部分。
  • 使用 componentDidCatch 可以提供更多的错误处理灵活性,因为它可以访问额外的错误信息。

通过合理使用 getDerivedStateFromErrorcomponentDidCatch,开发者可以提高 React 应用的健壮性,优雅地处理错误情况。

  1. 生命周期方法的改进:Fiber 支持新的生命周期方法 getDerivedStateFromPropsgetSnapshotBeforeUpdate,这些方法提供了更细粒度的控制,并且可以与类组件的现有生命周期方法结合使用。
  2. 记忆化渲染:Fiber 允许类组件通过 shouldComponentUpdate 方法进行更有效的更新检查,减少不必要的渲染。

对函数式组件的优化:

  1. Hooks:Fiber 架构的引入为函数式组件带来了 Hooks 的支持,使得函数组件能够使用状态和其他 React 特性,如 useStateuseEffectuseContext 等。
  2. 并发模式:在 React 18 中,Fiber 支持并发模式,允许函数组件利用多线程进行渲染,进一步提升性能。

通用优化:

  1. 增量渲染:Fiber 支持将渲染工作分割成小块,并分散到多个帧中,使得渲染过程更加平滑和响应。
  2. 任务优先级:Fiber 可以根据任务的优先级进行调度,确保高优先级的更新(如用户交互)优先处理。
  3. 双缓冲技术:Fiber 使用双缓冲机制,维护两个 Fiber 树——当前树和工作进度树,允许 React 在更新时随时进行比较、中断或恢复操作。

总的来说,React Fiber 为类组件和函数式组件都带来了显著的性能提升和新特性,使得开发者能够构建更高效、更稳定的 React 应用程序。

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值