React面试题

339 阅读13分钟

组件通信

在 React 中,组件通信是构建复杂应用的关键部分,主要有以下几种方式:

  1. 父组件向子组件通信:父组件通过向子组件传递属性(props)来实现通信。父组件在渲染子组件时,可以将数据作为属性传递给子组件,子组件通过接收这些属性来获取所需的数据和操作。
  2. 子组件向父组件通信:通常通过回调函数来实现。父组件将一个函数作为属性传递给子组件,子组件在特定情况下调用这个回调函数,并将需要传递的数据作为参数传递给父组件。
  3. 跨级组件通信:可以采用中间组件层层传递 props 的方式,但这种方式较为繁琐。更好的做法是使用上下文(Context)。通过创建和使用 createContext 对象,在父组件中使用 Provider 提供数据,然后在需要的子组件中使用 useContext 来获取数据。
  4. 非嵌套组件通信(兄弟组件通信):常见的方式是找到这两个兄弟组件的共同父组件,借助父组件作为中间桥梁来实现通信。或者使用全局状态管理工具,如 Redux 或 MobX ,将共享的状态存储在一个全局的 store 中,各个组件可以获取和修改这个状态。
  5. 发布 / 订阅模式:可以使用第三方库,如 PubSubJS ,组件发布事件,其他组件订阅该事件来实现通信。 在实际开发中,需要根据具体的业务场景和项目架构选择合适的组件通信方式,以确保应用的可维护性和性能

路由跳转的两种模式

在前端开发中,常见的路由跳转有两种模式:Hash 模式和 History 模式。

  1. Hash 模式: 在 URL 中,# 后面的部分被称为 Hash 值。Hash 值的变化不会导致浏览器向服务器发送请求,页面也不会重新加载。路由的切换通过改变 Hash 值来实现,浏览器会监听 Hash 值的变化,并根据变化更新页面的显示内容。
  2. History 模式: 利用 HTML5 的 History API 来实现路由跳转。通过 pushState、replaceState 方法来改变浏览器的历史记录,同时监听 popstate 事件来响应浏览器的前进、后退操作。这种模式可以使 URL 看起来更加美观和规范,但需要服务器端的配合来处理任意路径的访问,否则可能会出现 404 错误。

useEffect

函数组件使用生命周期钩子,监测所有的状态,如果第二个数组为空数组,表示谁也不监测 useEffect 相当于类组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 这几个生命周期函数的组合。

useEffect 中不指定依赖项时,其回调函数中的逻辑在组件挂载时执行,类似于 componentDidMount 。 当指定依赖项数组时,如果依赖项发生变化,回调函数会执行,类似于 componentDidUpdate 。 同时,可以在 useEffect 的回调函数中返回一个清理函数,用于执行一些清理操作,类似于 componentWillUnmount 中进行的资源释放等操作。

  1. 可以发射请求获取数据
  2. 设置订阅发布,启动定时器
  3. 手动更改正式的DOM

useRef和createRef的区别

引用DOM元素或DOM实例

区别:

useRef 是在 React 函数组件中使用的钩子,而 createRef 用于类组件。 useRef 会返回一个可变的 ref 对象,其 .current 属性可以用来存储引用的值,在组件的多次重新渲染中,useRef 返回的对象始终保持不变,只有 .current 属性的值可以更改。 createRef 则是在类组件的构造函数中调用,创建的 ref 对象可以被赋给组件中的元素,用于获取该元素的引用。

总的来说,它们的主要区别在于使用的组件类型不同,并且在更新引用值的方式和时机上可能会有细微差异。

useLayoutEffect

useLayoutEffect 是 React 中的一个钩子函数,其功能与 useEffect 类似,但在执行时机上有所不同。useLayoutEffect时机更提前一些

useEffect 的回调函数在浏览器完成页面渲染后异步执行,不会阻塞页面的更新显示。而 useLayoutEffect 的回调函数会在浏览器进行布局和绘制之前同步执行,这意味着它可能会阻塞页面的更新,导致用户能感觉到短暂的卡顿。 一般来说,如果你的副作用操作涉及到对 DOM 的测量、样式计算等可能会影响页面布局的操作,使用 useLayoutEffect 可以避免一些视觉上的闪烁或不一致。但由于它可能会阻塞页面渲染,所以要谨慎使用,避免在其中执行耗时过长的操作。 例如,如果需要根据 DOM 元素的尺寸立即更新样式,可能就适合使用 useLayoutEffect :

总的来说,useEffect 适用于大多数常见的副作用操作,而 useLayoutEffect 则更适用于那些对页面布局有即时性要求且执行较快的操作。

useMemo

useMemo 是 React 中的一个钩子函数,主要用于性能优化。 它接受两个参数,第一个参数是一个计算函数,用于生成需要缓存的值;第二个参数是一个依赖项数组。

useMemo 的作用是只有当依赖项数组中的值发生变化时,才会重新计算并返回新的值。如果依赖项没有变化,它会返回之前缓存的值,避免不必要的重复计算,从而提高性能。 在实际开发中,合理地使用 useMemo 可以减少不必要的计算,特别是在处理大型数据或复杂计算时,有助于提升应用的性能和响应性。

useCallback

useCallback 是 React 中的一个钩子函数,主要用于优化函数在组件中的传递和使用,以避免不必要的子组件重新渲染。

useCallback 接受两个参数,第一个参数是一个回调函数,第二个参数是依赖项数组。它会返回一个记忆化的回调函数,只有当依赖项发生变化时,才会重新创建这个回调函数。 ** 在父组件向子组件传递函数作为属性时,如果父组件重新渲染,而传递的函数没有使用 useCallback 进行处理,那么子组件可能会因为接收到新的函数引用而重新渲染,即使它所依赖的其他数据没有变化。通过 useCallback 对函数进行记忆化处理,可以避免这种不必要的重新渲染,从而提高性能。 **

useReducer

useReducer 是 React 钩子中的一个函数,用于管理组件中的复杂状态逻辑。 useReducer 接收两个参数,第一个参数是一个形如 (state, action) => newState 的 reducer 函数,第二个参数是初始状态。它返回当前的状态值和一个 dispatch 函数,用于触发状态的更新。

与 useState 相比,useReducer 更适用于状态逻辑较为复杂或状态的更新操作依赖多个值、包含异步操作或需要共享一些逻辑的时候。

例如,如果有一个购物车的状态管理,可能有添加商品、删除商品、修改商品数量等操作,就可以使用 useReducer 来清晰地组织这些状态更新逻辑:

使用 useReducer 可以使状态管理的逻辑更加集中、可维护和可测试

useContext

useContext 是 React 中的一个钩子函数,用于在组件树中便捷地共享和访问全局数据。它需要与 createContext 配合使用。首先,使用 createContext 创建一个上下文对象。然后,在需要提供数据的组件中,使用相应上下文对象的 Provider 组件来包裹子组件,并通过 value 属性传递数据。 在子组件中,可以使用 useContext 钩子来获取共享的数据,而无需通过多层组件逐层传递 props 。

useContext 的优点在于它简化了跨组件的数据共享流程,减少了代码的冗余性,使数据的传递更加直观和高效。

在实际开发中,useContext 常用于共享诸如主题、用户偏好、全局配置等数据。

组件生命周期

在 React 类组件中,生命周期方法可分为挂载、更新和卸载三个阶段。

挂载阶段的生命周期方法有:

constructor():用于初始化组件的状态和进行方法绑定。 componentWillMount():此方法在组件即将挂载到页面之前调用,但已不推荐使用。 render():这是必不可少的方法,用于定义组件要渲染的内容。 componentDidMount():组件挂载完成后调用,常用于进行数据获取、添加事件监听等副作用操作。

更新阶段的生命周期方法包括:

componentWillReceiveProps(nextProps):当组件接收到新的属性时调用,不过已不推荐使用。 shouldComponentUpdate(nextProps, nextState):通过返回 true 或 false 来决定组件是否需要更新,可用于性能优化。 componentWillUpdate(nextProps, nextState):在组件即将更新之前调用,已不推荐使用。 componentDidUpdate(prevProps, prevState):组件更新完成后调用,可在此处理基于更新的操作。

卸载阶段的生命周期方法是:

componentWillUnmount():在组件即将从页面中卸载时调用,用于清理定时器、取消事件监听等资源释放操作。 需要注意的是,随着 React 的发展和函数组件的广泛应用,新的钩子函数如 useEffect、useLayoutEffect 等提供了更灵活和直观的方式来处理副作用和组件更新逻辑,逐渐取代了部分传统的类组件生命周期方法。

总结: 在 React 的类组件中,生命周期方法经历了一些变化。在 16.3 之前,常见的生命周期方法包括构造函数 constructor 用于初始化状态和绑定方法,render 方法定义组件的渲染输出,componentDidMount 在组件挂载后进行副作用操作,如数据获取等,shouldComponentUpdate 用于决定组件是否更新以优化性能,componentDidUpdate 在组件更新后执行相关操作,componentWillUnmount 用于组件卸载时清理资源。

然而,在 16.3 及之后,引入了新的生命周期方法,如 getDerivedStateFromProps 用于根据新的属性更新状态,getSnapshotBeforeUpdate 用于在更新前获取一些快照信息。但需要注意的是,随着函数组件和钩子的广泛应用,现在更多倾向于使用函数组件和诸如 useEffect 等钩子来处理组件的逻辑和副作用,因为它们提供了更简洁和灵活的方式来管理组件的状态和生命周期。

React 错误边界处理

错误边界是一种特殊的 React 组件,用于捕获其子组件树在渲染、生命周期方法或子组件构造函数中抛出的错误。 要创建一个错误边界组件,需要定义两个生命周期方法:getDerivedStateFromError 和 componentDidCatch 。 getDerivedStateFromError 方法接收错误对象作为参数,并返回一个更新后的 state,用于决定组件要渲染的内容,通常是一个错误提示界面。 componentDidCatch 方法接收错误对象和错误信息对象作为参数,可以在此执行一些诸如日志记录、错误报告等额外的操作。 例如:

  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 来显示错误界面
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 可以在这里记录错误日志等
    console.log(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

然后,将可能会出错的子组件包裹在错误边界组件中使用。 这样,当被包裹的子组件发生错误时,错误边界组件可以捕获并处理这些错误,避免整个应用崩溃,并提供友好的错误提示。

React 代码分割

在 React 中,代码分割指的是将一个大型的应用代码拆分成多个较小的代码块,并按需加载这些代码块,而不是在应用初始加载时就加载全部代码。 代码分割主要有以下几个好处:

  1. 提高初始加载性能:用户在访问页面时,无需等待整个应用的所有代码都加载完成,只加载当前页面所需的关键代码,从而减少初始加载时间,提高用户体验。
  2. 优化资源利用:根据用户的操作和页面访问路径,有针对性地加载相应的代码,避免不必要的资源浪费。
  3. 更好的缓存策略:分割后的代码块可以更精细地进行缓存控制,提高缓存的命中率。

在 React 中,常见的实现代码分割的方式有:

  1. 使用动态 import :通过在组件内部使用动态 import 语句来按需加载模块。
  2. 使用 React Router 的路由懒加载:在配置路由时,可以将路由对应的组件设置为懒加载。 例如:
const MyComponent = React.lazy(() => import('./MyComponent'));

// 路由懒加载
const routes = [
  {
    path: '/myRoute',
    component: React.lazy(() => import('./MyRouteComponent'))
  }
];

总之,代码分割是优化 React 应用性能和用户体验的重要手段。

setState 更新机制(是同步更新还是异步?)

setState并非严格意义上的异步,只是在多数情况下表现得像是异步。在源码中,通过isBatchInUpdates来判断是先将更新放入队列还是直接更新。若isBatchInUpdates值为true,则进行异步操作,先将更新放入队列,后续统一处理;若值为false,则直接更新。 例如,在 React 的生命周期和合成事件中,通常会走合并操作,延迟更新,这是为了性能优化以及保持状态更新的一致性。如果在 React 无法控制的场景,如原生的addEventListener、setInterval、setTimeout等操作中,setState则是同步更新。 异步更新主要有两个原因:一是为了性能优化,避免频繁的不必要的重新渲染;二是为了实现并发更新和异步渲染,以保持应用的流畅性和响应性。同时,如果将setState改为同步,会导致与props的更新不一致,这在实际应用中是不可行的。

Fiber

Fiber 是 React 16 引入的核心架构。 Fiber 的主要目标是解决之前版本在渲染大型复杂组件树时可能出现的性能和用户体验问题。 它带来了以下几个重要的改进:

  1. 增量渲染:Fiber 将渲染工作分割成小的任务单元,能够在每一帧的时间内暂停和恢复渲染工作,从而不会阻塞浏览器的主线程,保证页面的交互性。
  2. 优先级调度:可以为不同的更新任务设置优先级,优先处理更紧急和重要的更新,例如用户交互相关的更新。
  3. 更高效的协调算法:通过新的算法更有效地对比新旧虚拟 DOM,减少不必要的重新渲染,提高渲染性能。 从实现角度来看,Fiber 为每个组件创建了一个 Fiber 节点,这些节点形成了一个链表结构,包含组件的各种信息,如类型、属性、状态等,方便进行高效的更新和协调操作。 总之,Fiber 的引入极大地提升了 React 应用的性能和用户体验,使 React 能够更好地应对复杂和大规模的应用场景