Skip to content
On This Page

面试题[React]

1. React 基础知识

1.1 React 框架的核心思想

  1. 组件化
    • 构建可复用的 UI 组件,并将其组合成复杂的用户界面。
    • 在 React 中,UI 被拆分成独立的、可复用的组件,每个组件都封装了自己的逻辑和样式。这样不仅提高了代码的可维护性和复用性,还使得开发和调试变得更加容易。
  2. 虚拟 DOM
    • 通过虚拟 DOM 提高 DOM 操作的性能,使得 React 的渲染过程高效、快速。
    • React 使用虚拟 DOM 来减少直接操作真实 DOM 的次数。当状态发生变化时,React 首先更新虚拟 DOM,然后进行“diffing”算法来找出改变的部分,最后只更新那些实际发生变化的元素。这大大提高了渲染性能,避免了不必要的 DOM 操作。
  3. 单向数据流
    • 数据在组件间的流动是单向的,父组件通过 props 向子组件传递数据和回调函数,而子组件不能直接修改父组件的状态。
    • 单向数据流确保了数据的流动方向清晰明了,即数据只能从父组件向子组件流动。父组件通过 props 向子组件传递数据和函数,子组件可以调用这些函数来请求更新父组件的状态,但不能直接修改父组件的状态。这种模式帮助我们避免了数据的不一致性和复杂性,提高了应用的可预测性。
  4. 声明式编程
    • 使用声明式的方式来构建 UI,简化了开发中的逻辑控制。
    • 在 React 中,我们使用 JSX 来描述 UI 应该是什么样子,而不是如何去实现这个 UI。通过声明式编程,我们更关注组件的最终状态而不是过程。这种方式使代码更加直观、简洁,降低了出错的几率。

1.2 类组件和函数式组件的区别

类组件(Class Component)和函数式组件(Functional Component)是 React 中定义组件的两种主要方式,它们在使用方式和功能上有一些关键的区别:

  1. 定义方式:类组件是通过 ES6 类语法创建的,必须继承自 React.Component。函数组件则是普通的 JavaScript 函数。
  2. 状态和生命周期:类组件有内置的状态(state)和生命周期方法(如 componentDidMountcomponentDidUpdate 等),函数组件最初没有状态和生命周期。不过,自从引入了 React Hooks(如 useStateuseEffect)之后,函数组件也能拥有类似的状态管理和生命周期功能。
  3. 性能:函数组件通常在性能上优于类组件,因为它们更轻量级,没有实例化的过程。
  4. 代码简洁度:函数组件代码通常更简洁和直观。尤其配合 Hooks 使用时,更容易读懂和维护。

1.3 JSX

JSX 是 JavaScript 语法的扩展,它允许编写类似于 HTML 的代码。它可以编译为常规的 JavaScript 函数调用,从而为创建组件标记提供了一种更好的方法。

jsx
// JSX代码如下
<div className="sidebar" />
javascript
// 转换为以下JS代码
React.createElement(
  'div',
  {className: 'sidebar'}
)
  1. React 是否必须使用 JSX

    React 并不必须使用 JSX。JSX 是一种 JavaScript 的语法扩展,它用来描述用户界面的外观和结构。尽管 JSX 使得代码更加简洁和直观,但实际上,React 元素可以通过纯 JavaScript 使用 React.createElement 方法来创建。

    简单两句来总结就是:

    • JSX 只是 React 的一种语法糖,使用它可以让代码更具可读性。
    • React 本身只需要 JavaScript,不需要 JSX 来工作。
  2. 为什么使用 JSX

    虽然使用纯 JavaScript 也完全可以编写 React 组件,但 JSX 有几个显著优势:

    • 可读性强:它使得代码更易于理解,尤其是涉及复杂结构的用户界面时。
    • 开发效率高:由于 JSX 更加简洁明了,开发人员可以更快地进行开发和调试。
    • 社区支持:JSX 是 React 库的推荐写法,许多工具和库(如 Babel)都对 JSX 提供了良好的支持。

1.4 React Hooks

React Hooks 是 React 16.8 版本引入的一项特性,它允许在函数式组件中使用状态(state)和其他 React 特性,而不需要使用类组件。以下是对 React Hooks 的详细解释:

  1. 基本概念

    Hooks 就是钩子,作用是把某个目标结果“钩”到某个可能会变化的数据源或者事件源上。那么当被“钩”到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果。

  2. React Hooks 带来的优势

    • 状态逻辑复用:通过自定义 Hooks,可以轻松地在多个组件之间复用状态逻辑。
    • 代码简洁性:Hooks 使得函数组件变得更加简洁和易于理解,避免了类组件中的复杂生命周期方法和状态管理。
    • 函数式编程:Hooks 拥抱了函数式编程,使得 React 组件的开发更加符合现代 JavaScript 的开发趋势。
  3. 注意事项

    • 避免过度使用Hooks:虽然Hooks提供了强大的功能,但过度使用可能会导致代码难以理解和维护。因此,在合适的时候使用Hooks是很重要的。
    • 注意Hooks的调用顺序:在函数组件中,Hooks的调用顺序必须是固定的。在组件的不同渲染中,Hooks的调用顺序应该保持一致。
    • 不要在循环、条件或嵌套函数中调用Hooks:这是React的一个规则,必须遵守以避免潜在的错误。
  4. 解决了哪些问题

    • 数据抽象生命周期
    • 组件间状态逻辑复用困难。
    • 复杂组件难以理解。
    • class 组件的学习和维护成本。
    • 提供了更简洁和易于理解的代码。
    • 更灵活的组件设计。
    • 更易于测试。

1.5 React Suspence

会将 html 的读取过程变成 chunk 模式。(分块传输编码)

Suspense 是 React 提供的一个组件,用于处理异步操作,比如数据加载、代码分割等等。最初引入的时候,React 团队的目标是优化用户体验,尤其是在处理异步资源时。通过 Suspense,你可以让 React 在获取必要数据或代码之前,显示一个备用内容或加载状态,以提升用户体验。

具体而言,Suspense 组件解决了以下几个问题:

  • 1)异步资源加载:当某个组件需要异步获取数据或模块时,Suspense 可以优雅地显示加载状态。
  • 2)用户体验:通过在数据或代码加载完之前提供一个占位符,Suspense 改善了在网络状况较差或数据获取较慢时的用户体验。
  • 3)代码分割:配合 React.lazy 使用,Suspense 可以更方便地进行代码分割,减少初始加载时间。

1.6 React Context

React Context是React中一个重要的特性,它提供了一种在组件树中共享数据的方法,而无需通过每个层级显式地传递props。以下是对React Context的详细解释:

  1. 基本概念

    • 定义:Context是React的一个API,它允许你在组件树中传递数据,而不必在每一层组件上显式地通过props传递
    • 作用:Context的主要作用是解决在React应用中,当某些数据需要在组件树中的多个层级之间共享时,通过props逐层传递的繁琐问题。
  2. 创建和使用

    1. 创建Context:使用React.createContext()方法创建一个Context对象。这个对象包含两个重要的组件:ProviderConsumer
      • Provider:用于提供Context的值。它接收一个value属性,这个属性的值会被传递给所有订阅了这个Context的组件。
      • Consumer:用于消费Context的值。它是一个函数组件,接收一个函数作为子元素,这个函数会接收当前的Context值作为参数。
    2. 使用Context
      • 在类组件中使用:可以通过设置static contextType属性来指定要消费的Context,然后在组件内部通过this.context来访问Context的值。
      • 在函数组件中使用:可以使用useContextHook来访问Context的值。useContext接收一个Context对象作为参数,并返回当前Context的值。
  3. 使用场景

    • 跨级传递数据:当需要在组件树中跨越多层组件传递数据时,可以使用Context来避免逐层传递props的繁琐。
    • 全局状态管理:Context可以用于管理全局状态,如当前认证的用户、主题或首选语言等。这些状态通常需要在多个组件之间共享。
    • 避免不必要的组件层次:通过Context,可以避免将数据传递到不需要它的组件中,从而减少组件的层次和复杂度。
  4. 注意事项

    • 默认值:当组件所处的树中没有匹配到Provider时,Context的defaultValue参数会生效。这有助于在不使用Provider包装组件的情况下对组件进行测试。但请注意,将undefined传递给Provider的value时,消费组件的defaultValue不会生效。
    • 嵌套使用:多个Provider可以嵌套使用,里层的Provider会覆盖外层的数据。
    • 性能优化:虽然Context提供了一种方便的数据传递方式,但过度使用可能会导致性能问题。因此,在使用Context时,应尽量避免不必要的订阅和更新。
  5. 示例代码

    jsx
    import React, { createContext, useContext, useState } from 'react';
    
    // 创建一个Context对象
    const ThemeContext = createContext();
    
    // 使用Provider提供Context的值
    function App() {
      const [theme, setTheme] = useState('light');
    
      return (
        <ThemeContext.Provider value={theme}>
          <Toolbar />
          <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
            Toggle Theme
          </button>
        </ThemeContext.Provider>
      );
    }
    
    // 消费Context的值
    function Toolbar() {
      // 使用useContext Hook访问Context的值
      const theme = useContext(ThemeContext);
    
      return <ThemedButton theme={theme} />;
    }
    
    function ThemedButton({ theme }) {
      return <button style={{ backgroundColor: theme === 'light' ? 'white' : 'black' }}>
        Button
      </button>;
    }
    
    export default App;

    在这个示例中,我们创建了一个ThemeContext来管理主题状态。在App组件中,我们使用ThemeContext.Provider来提供主题的值,并通过按钮来切换主题。在Toolbar组件中,我们使用useContextHook来访问主题的值,并将其传递给ThemedButton组件。这样,我们就实现了跨级传递数据的功能。

1.7 React 事件机制原理

React 的事件机制是指 React 对浏览器原生事件系统的一层封装和改进。其核心是 "合成事件" (SyntheticEvent),通过事件代理的方式将所有事件统一处理,从而提升性能和跨浏览器兼容性。

1)合成事件(SyntheticEvent):React 会将浏览器的原生事件包装成一个合成事件对象。这个对象具有与原生事件对象相同的接口,但跨浏览器兼容性更好。

2)事件代理:React 会将组件内的所有事件监听统一附加到最外层的 DOM 容器(通常是 document)。当事件发生时,由这个顶层节点捕捉事件并进行分发,而不是每个组件都各自处理事件。

React 的事件机制原理主要涉及到事件注册、事件合成、事件冒泡、事件派发等关键步骤,它虽然与原生 DOM 事件机制有所不同,但仍然是基于浏览器的事件机制完成的。以下是 React 事件机制原理的详细解释:

  1. 事件注册:
    • React 并不将事件直接绑定到具体的 DOM 节点上,而是统一绑定到 document
    • 当组件挂载时,React 会根据组件内声明的事件类型(如 onClickonChange 等),给 document 添加事件监听,并指定统一的事件处理程序 dispatchEvent
    • React 会将 React 组件内所有事件统一存放到一个对象(如 listenerBank)内,以便在触发事件时能够找到对应的方法去执行。
  2. 事件合成(SyntheticEvent):
    • React 使用 SyntheticEvent 作为所有事件的基类,该类定义了合成事件的基础公共属性和方法。
    • React 会根据当前的事件类型来使用不同的合成事件对象,如 SyntheticMouseEvent(鼠标事件)、SyntheticFocusEvent(焦点事件)等。
    • SyntheticEvent 是 React 对原生事件的封装,处理了浏览器兼容问题,并且具有与原生事件相同的接口,如 stopPropagation()preventDefault()
  3. 事件冒泡:
    • React 中的事件传播遵循与原生 DOM 事件相似的机制,包括捕获阶段和冒泡阶段。
    • 在 React 中,事件传播是通过组件的 props 来实现的。当一个组件触发一个事件时,React 会调用该组件的事件处理函数,并将事件对象作为参数传递给该函数。
    • 默认情况下,React 中的事件处理程序在冒泡阶段被触发。如果需要处理捕获阶段的事件,可以使用如 onClickCapture 的事件处理函数。
  4. 事件派发(dispatchEvent):
    • React 通过 dispatchEvent 方法统一进行事件的派发。当事件触发时,React 会根据当前的组件 ID 和事件类型找到对应的事件回调函数并执行。
    • React 中的事件传播可以用于实现组件之间的通信和交互,如父组件可以通过 props 将事件处理函数传递给子组件,在子组件中触发该事件时,父组件可以捕获到该事件并执行相应的逻辑。
  5. 其他特点:
    • React 中的事件对象 e 是一个合成对象,而不是原生的事件对象。这个对象已经被 React 封装过,并且处理了浏览器兼容问题。
    • React 的事件命名方式采用驼峰命名法(如 onClick),与 DOM 的命名方式(如 onclick)有所不同。
    • 在 React 中,不能通过返回 false 来阻止默认行为或阻止事件冒泡。需要显式调用 e.preventDefault()e.stopPropagation() 来实现这些功能。

总结来说,React 的事件机制原理通过事件注册、事件合成、事件冒泡和事件派发等步骤实现了对事件的高效处理和管理。这种机制减少了内存消耗、提高了性能,并且为开发者提供了统一的规范和友好的开发体验。

1.8 React 异步机制

React的异步机制是其性能优化的关键部分,其底层逻辑主要涉及到调度器(Scheduler)、协调器(Reconciler)以及Fiber架构。以下是对React异步机制底层逻辑的详细解释:

  1. 调度器(Scheduler)

    调度器是React中负责任务调度的部分。它根据任务的优先级来决定任务的执行顺序,确保高优先级的任务能够优先得到执行。这种机制使得React在面对大量更新时,能够保持界面的流畅性,避免卡顿。

  2. 协调器(Reconciler)

    协调器是React中负责对比新旧虚拟DOM(VDOM),并计算出最小DOM变化的部分。在React 16及以后的版本中,协调器采用了Fiber架构来实现异步更新。

  3. Fiber架构

    Fiber是React的最小工作单元,它代表了一个可以中断和恢复的更新过程。Fiber架构使得React能够将更新过程拆分成多个小的任务,并在浏览器的空闲时间中逐个执行这些任务。这种机制避免了长时间的阻塞操作,提高了应用的响应性。

    具体来说,Fiber架构通过以下方式实现异步更新:

    1. 双树结构:Fiber架构中维护了两棵树,一棵是当前渲染的树(current Fiber Tree),另一棵是正在内存中构建的树(workInProgress Fiber Tree)。这两棵树通过alternate属性互相指向,方便在更新完成后进行切换。
    2. 任务拆分:React会将更新过程拆分成多个小的任务(即Fiber节点),每个任务都包含了一定的更新逻辑。这些任务被放入一个任务队列中,等待调度器进行调度。
    3. 时间片分配:调度器会根据任务的优先级和浏览器的空闲时间,为每个任务分配一个时间片。在时间片内,React会尽可能多地执行任务,直到时间片用完或任务队列为空。
    4. 中断与恢复:如果在一个时间片内任务没有执行完,React会将当前任务的状态保存到内存中,并等待下一个时间片继续执行。这种机制使得React能够在浏览器的空闲时间中逐步完成更新过程,而不会阻塞其他操作。
  4. MessageChannel

    React Scheduler 使用 MessageChannel 的目的就是为了产生宏任务。为了实现:

    1. 将主线程还给浏览器,以便浏览器更新页面。
    2. 浏览器更新页面后继续执行未完成的任务。
  5. 异步更新的优势

    1. 提高性能:通过拆分任务和分配时间片,React能够充分利用浏览器的空闲时间进行更新操作,避免了长时间的阻塞操作,提高了应用的性能。
    2. 优化用户体验:异步更新机制使得React能够更快地响应用户的输入和操作,减少了界面的卡顿和延迟,提高了用户体验。
    3. 支持高优先级任务:调度器能够根据任务的优先级进行调度,确保高优先级的任务能够得到优先执行,进一步提高了应用的响应性和稳定性。

综上所述,React的异步机制通过调度器、协调器和Fiber架构的协同工作,实现了高效的异步更新过程。这种机制不仅提高了应用的性能,还优化了用户体验,使得React在面对大量更新时能够保持界面的流畅性和稳定性。

1.9 React 生命周期

React 的生命周期主要分为三个阶段:挂载阶段(Mounting)、更新阶段(Updating)和卸载阶段(Unmounting)。每个阶段都有对应的生命周期函数。

  1. 挂载阶段(Mounting):组件被创建并插入到 DOM 中。
    • constructor(): 这个函数是类组件的初始化方法。可以在这里初始化状态或者绑定事件处理方法。
    • static getDerivedStateFromProps(): 在每次渲染之前都会调用这个生命周期方法,允许组件基于接收到的属性更新内部状态。它是静态方法,因此无法访问 this
    • render(): 这是一个纯函数,根据组件的状态和属性,返回要渲染的 JSX 内容。
    • componentDidMount(): 组件已经被插入到 DOM 中。这里适合进行 DOM 操作或者发起网络请求。
  2. 更新阶段(Updating):组件的状态或属性发生变化时,会重新渲染。
    • static getDerivedStateFromProps(): 与挂载阶段中的作用相同。
    • shouldComponentUpdate(): 在渲染之前调用,可以让你控制组件的更新。默认返回 true,如果返回 false,组件就不会更新。
    • render(): 执行实际的渲染操作。
    • getSnapshotBeforeUpdate(): 在最新的渲染结果提交到 DOM 之前调用,可以捕获当前 DOM 的状态,比如滚动位置。
    • componentDidUpdate(): 组件的更新已经反映到 DOM 中,这里可以进行 DOM 操作或者网络请求。注意,要避免在这里直接调用 setState() 引发无休止的循环更新。
  3. 卸载阶段(Unmounting):组件即将被从 DOM 中移除。
    • componentWillUnmount(): 组件将要被从 DOM 中移除。这个函数通常用于清理资源,比如取消网络请求或移除事件监听器。

新版本 React (16.3 之后) 引入了一些新的生命周期方法:

  • static getDerivedStateFromError()componentDidCatch(): 错误边界相关的生命周期方法,用于捕获子组件渲染时发生的错误。

React Hooks: 如果你使用函数组件,可以通过 hooks 来模拟生命周期方法:

  • useEffect():可以模拟 componentDidMountcomponentDidUpdatecomponentWillUnmount 三个阶段。
  • useState():可以管理组件的状态。

所以,对于用 hooks 的函数组件,整个生命周期的流程还是可以被很好的模拟和处理的,只是方式不同。

1.10 React 组件通信

在 React 中,组件间共享数据其实是一个常见的需求。主要有以下几种方法:

  1. 通过父组件传递 Props

    这是最基础的方法,也是 React 最推荐的方式。当子组件需要共享某些数据时,我们可以把这些数据提升到最上层的共同父组件,然后通过 props 逐层向下传递。这种方式虽然直观但是在组件层级过深时会显得繁琐,特别是所谓的 "props drilling" 现象。

  2. 使用 Context API

    Context API 是为了解决 props drilling 引入的。通过使用 React.createContext() 来创建一个全局的 context,然后通过 Provider 和 Consumer 来供给和消费数据。这种方式适用于不需要有复杂操作的共享数据情境,能够有效简化层层传递的逻辑。

  3. 使用 Redux 或 MobX 等外部状态管理库

    当应用规模变大,一些局部的状态管理方法显得不再适用时,使用 Redux 或 MobX 这样的外部状态管理库是一个良好的选择。Redux 是基于 Flux 架构的,它采用单一 store,所有的状态变化都是通过 action 显式地分发,从而保持整个应用状态的一致性。MobX 则是一个使得状态管理变得更加简便和透明的库,通过可观察的数据流使得状态变化更加直观。

  4. 使用 React Hooks

    Hooks 是 React 提供的一种更灵活的函数组件状态管理和副作用逻辑的方法。我们可以自定义 hooks(例如 useCustomHook)使得在多个组件之间共享状态变得更加容易和直观。Hooks,例如 useState 和 useReducer,非常适用比例较小的应用或组件维度的状态管理。

  5. 使用全局事件系统(譬如 EventEmitter)

    对于一些场合,可以使用事件机制来管理状态变化,例如 Node.js 提供的 EventEmitter。虽然这种方式比较灵活,但通常不推荐,这样做的结果是会增加复杂度和降低代码的可维护性,特别是在大型应用程序中。

1.11 React 状态提升

React 的状态提升(State Lifting)是指将子组件的共享状态提升到其共同的父组件中,以便父组件可以管理这个状态,并将其作为属性(props)传递给子组件。这样做的目的是为了保证多个组件能够共享和使用同一个状态,使得状态管理更加集中和便捷。

使用场景主要包括以下几个:

  • 1)当多个组件需要共享某一状态时。
  • 2)当父子组件之间的状态需要同步或者协同工作时。
  • 3)当子组件的状态变化需要影响到更高层级的其他组件时。

优点与注意事项:

  • 1)集中管理: 通过状态提升,多个组件可以方便地共享和同步状态,使得状态管理更加集中且维护容易。
  • 2)数据流向清晰: 状态提升后,数据单向流动(从父组件传递到子组件),使得组件间的关系和数据流向变得更加简单明了,易于调试和排查问题。
  • 3)性能考虑: 状态提升可能会导致父组件在状态改变时重新渲染所有子组件,因此需要注意性能优化,避免不必要的渲染。

1.12 React 的错误边界 Error Boundary

1.13 setState

在 React 中,setState() 函数通常被认为是异步的,这意味着调用setState() 时不会立刻改变 React 组件中 state 的值,setState 通过触发一次组件的更新来引发重绘,多次 setState 函数调用产生的效果会合并。

调用 setState 时,React 会做的第一件事情是将传递给 setState 的对象合并到组件的当前状态。这将启动一个称为和解(reconciliation)的过程。和解(reconciliation)的最终目标是以最有效的方式,根据这个新的状态来更新 UI。 为此,React 将构建一个新的 React 元素树(您可以将其视为 UI 的对象表示)。一旦有了这个树,为了弄清 UI 如何响应新的状态而改变,React 会将这个新树与上一个元素树相比较。

  1. 在 React 中,setState 的执行时机依赖于它所在的执行环境。具体来说:

    • React Fiber 的影响: 在新的 React Fiber 架构中,异步渲染提供了更精细的控制,使得 setState 的行为更加高效。Fiber 允许中断渲染和继续执行,更好地调配资源,这也是为什么 setState 的更新有时表现为异步。
    • 异步情况:当 setState 在 React 的生命周期方法(如 componentDidMountcomponentDidUpdate)或者合成事件(如捕获用户操作的事件处理函数)中被调用时,setState 是异步的。这种异步性是为了提高性能,避免频繁渲染和批处理多个 setState 调用。
    • 同步情况:当 setState原生事件处理函数(如 addEventListener 注册的回调)或者定时器(如 setTimeoutsetInterval)中被调用时,setState 是同步的。这是因为这些回调并不在 React 的合成事件系统或者生命周期管理之中。
  2. 执行机制和实现原理

    在 React 中,setState 是一个用于更新组件状态的函数。其执行机制可以归纳为以下几个重要步骤:

    • 1)单向数据流setState 触发状态更新,从而引起组件重新渲染。React 采用单向数据流,即状态从父组件流向子组件。
    • 2)合并更新:React 会对多次调用 setState 进行合并,批量处理更新,从而提升性能。
    • 3)异步更新:在某些情况下(比如事件处理函数),setState 是异步的,以便批量处理多个状态更新请求。
    • 4)触发重新渲染:每次状态更新都会引起组件及其子组件的重新渲染,除非 shouldComponentUpdateReact.memo 返回 false
    • 5)调用生命周期方法:状态更新会触发组件的生命周期方法(如 componentWillUpdatecomponentDidUpdate)。

    以上是 setState 的最关键执行机制。

  3. 批处理机制

    React 通过批处理机制优化多次状态更新,比如在事件处理函数中,连续调用 setState,它们并不会立即触发组件重新渲染,而是会被合并成一次更新。这是因为 React 会给事件处理过程加个标志,以实现批处理。

    更新队列:React 内部维护了一个更新队列,每次调用 setState,新的状态会被加入到更新队列中。然后在适当的时候,React 会批量处理这个队列中的状态变化,从而减少不必要的重新渲染。

1.14 直接修改 state 有什么问题

在 React 中,通过 this.state 直接修改 state 是不推荐的:

  1. 绕过 React 的管理机制:直接修改 state 会绕过 React 的状态管理逻辑,导致 React 无法追踪和应用这些改动。
  2. 无法触发渲染:React 依赖 state 的改变来重新渲染组件,直接修改 state 不会启动这种过程,所以 UI 不会更新。
  3. 调试困难:直接修改 state 后,调试过程会变得更加复杂,因为无法通过 React 开发工具看到实时变化。

1.15 组件之间传递状态的方式

在React中,组件之间传递状态(或称为数据)有几种主要的方式。以下是这些方式的详细解释和归纳:(跨组件共享数据

  1. 父组件向子组件传递状态:
    • 主要通过props进行传递。父组件将需要传递的状态作为props的一部分传递给子组件,子组件通过this.props(在类组件中)或函数参数(在函数组件中)访问这些props。
  2. 父组件向更深层的子组件传递状态:
    • 如果需要向更深层的子组件传递状态,可以通过props的逐层传递来实现,但这可能会导致代码冗余和不易于维护。
    • 使用Context API是一种更好的选择。Context提供了一个无需显式地通过每一个层级手动传递props的方式,就能将值深入组件树的手段。在父组件中创建一个Context,将需要传递的状态存储在Context中,然后通过<Context.Provider>将状态提供给子组件,子组件可以通过<Context.Consumer>useContext Hook来访问这个状态。
  3. 子组件向父组件传递状态:
    • 主要通过回调函数进行传递。在父组件中定义一个回调函数,并将其作为props传递给子组件。子组件可以调用这个回调函数,并将需要传递的状态作为参数传递给它。这样,父组件就可以在回调函数中获取到子组件传递的状态。
  4. 没有任何嵌套关系的组件之间传递状态:
    • 例如,兄弟组件之间的状态传递,可以通过它们共同的父组件作为中介,利用父组件向子组件传递状态的方式,再结合子组件向父组件传递状态的方式,实现兄弟组件之间的状态传递。
    • 使用Redux、MobX等全局状态管理库也是一种常见的选择。这些库可以在应用的顶层创建一个全局状态存储,并在任何组件中访问和更新这个状态。
  5. 传递组件本身作为参数:
    • 这并不直接涉及状态的传递,但也是一种在React中传递组件的方式。可以通过传递JSX元素、直接传递组件本身,或者传递一个返回组件的函数来实现。这种方式主要用于动态渲染和组件复用。
  6. 使用Hooks(如useStateuseReducer
    • Hooks是React 16.8及以后版本提供的一种新特性,用于在函数组件中添加状态和其他React特性。虽然Hooks本身并不直接涉及组件之间的状态传递,但它们提供了一种在函数组件中管理状态的方式,从而可以与上述的传递方式结合使用。

综上所述,React组件之间传递状态的方式主要包括通过props的传递、使用Context API、通过回调函数、利用全局状态管理库以及传递组件本身作为参数等方式。具体使用哪种方式取决于应用的需求和架构。

1.16 受控组件和非受控组件的区别

  • 受控组件:由 prop 驱动
  • 非受控组件:由自定义组件内部的 state 驱动

1.17 如何操作虚拟 DOM 的 class 属性

在 React 中,为了操作虚拟 DOM 的 class 属性,一般会使用 className 属性,而不是直接操作 class 属性。React 使用 className 来代替传统的 HTML 属性 class,这是因为 class 是 JavaScript 的保留字。

jsx
<div className="my-class">Hello, World!</div>

1.18 mixins 有什么作用

React 中的 mixins 主要用于在多个组件之间共享行为和功能。Mixins 通过定义一组可以在不同组件中复用的方法和属性,来避免代码重复,并确保组件之间的一致性。它们特别适用于需要扩展组件功能而不想重复代码的场景,例如日志记录、数据获取、事件处理等。

Mixins 的作用和适用场景可以进一步探讨:

  1. 共享行为: Mixins 允许我们在多个组件之间共享相同的行为和方法。比如,一个通用的表单验证功能可以被封装在一个 Mixin 中,然后在不同的表单组件中使用,从而避免代码重复。
  2. 代码复用: 通过 Mixins,可以将通用的逻辑抽取出来,并在不同组件之间重用。例如,多个组件需要从同一个 API 获取数据,这时候,我们可以将这个数据获取的逻辑放在一个 Mixin 中,各个组件都能复用这一逻辑。
  3. 实现通用功能: 一些通用的功能,比如日志记录、事件监听、组件生命周期的特定处理等,都可以通过 Mixins 来实现。例如,我们可以编写一个日志记录的 Mixin,所有需要记录日志的组件都可以引用这个 Mixin。值得注意的是:
    • Mixins 在早期 React 版本中(例如 React 0.13)是常用的,但是由于其带来的一些设计上的复杂性和维护上的问题,后来逐渐被 React 官方不推荐使用。
    • 现在推荐使用的是 高阶组件(HOC)Render Props 来替代 Mixins。这些模式能够更好地复用代码,同时也更加符合 React 的组件化思想。

1.19 调用 super(props) 的目的

在 React 构造函数中调用 super(props) 是为了确保我们能够在子类中成功调用父类的构造函数,并能在构造函数中通过 this.props 正确访问到传入的 props。简而言之,它确保了继承的正确性和 props 的初始化

1.20 render 函数的原理

在 React 中,render 函数是 React 组件生命周期的一个重要部分,它的主要作用是描述组件的 UI(即其展示逻辑)。React 会根据 render 函数返回的描述来生成并更新真实的 DOM 元素,从而通过最小的代价实现高效的 UI 更新。具体步骤包括:

  • 1)组件初次渲染时,React 会调用 render 函数生成虚拟 DOM 结构,即一棵由 React 元素(对象)组成的树。
  • 2)React 进一步将虚拟 DOM 转换为真实 DOM,并将其插入页面。
  • 3)在组件的状态(state)或属性(props)变化时,render 函数会重新执行,产生一个新的虚拟 DOM。
  • 4)React 使用一种叫做 "Diffing" 的算法,对比新旧虚拟 DOM,找出变化的部分。调和是 React 应用 Diff 算法更新组件的过程。React 在调和过程中可以确保只修改必要的部分,从而最大程度地减少 DOM 操作,提高性能。
  • 5)最终,React 根据这些差异,最小化更新真正的 DOM,实现高效渲染。

1.21 render 函数的触发

React 的 render 函数一般在以下几种情况下会被触发:

  • 1)当组件的 state(状态) 发生变化时。
  • 2)当组件的 props(属性)发生变化时。
  • 3)当父组件的 render 函数被触发时,即使当前组件的 props 和 state 没有变化,也会重新渲染。这个情况经常会导致一些不必要的重复渲染,为此,我们可以使用一些优化手段,如 React.memo、PureComponent 或 shouldComponentUpdate。

1.22 render 函数返回的数据类型是什么

在 React 中,render 函数返回的是一个 React 元素(React Element)。React 元素是用来描述你希望在屏幕上看到的内容,它可以是一个普通的 HTML 元素,也可以是一个自定义的组件类型。

  1. React Element:这是 Render 函数的主要返回值。React 元素是一个普通的对象,它表示一个 UI 片段。有了这个元素,React 会根据它来构建虚拟 DOM,然后再根据虚拟 DOM 更新真实 DOM。通过调用 React.createElement 或使用 JSX 语法,我们可以创建这个元素。
  2. Component 或 Fragment:render 函数不仅可以返回原生 HTML 元素,还可以返回自定义组件或 React.Fragment。Fragment 是 React 用来返回多个子元素而不需要添加额外节点的解决方案。
  3. Array 或 null:在一些情况下,render 函数可以返回一个数组(包含多个 React 元素),甚至可以返回 null 表示不渲染任何内容。

1.23 在 render 方法中访问 refs

在 React 中,不建议render 方法中访问 refs。虽然技术上是可能的,你可以通过调用 this.refs 来访问某个 ref,但由于 refs 在生命周期钩子中是动态变化的,这种做法通常会导致不稳定和意料之外的行为。通常更推荐在 componentDidMountcomponentDidUpdate 这些生命周期方法中访问 refs

  1. 生命周期钩子和 ref 的挂载时机
    • 在 React 中,Refs 是在组件挂载后才创建。因此,如果你在 render 方法中尝试访问 refs,结果很可能是 undefined,因为此时组件的实际 DOM 元素并未真正放置在页面上。
    • render 方法的设计意图是纯粹的渲染,不应包含对 DOM 的直接操作,这样可以确保渲染是纯函数。
  2. 在正确的生命周期方法中使用 refs
    • componentDidMount:在组件被插入到实际的 DOM 之后调用。因此在这个方法中,你完全可以安全地访问和操作 refs
    • componentDidUpdate:此钩子在组件更新之后立即调用,适合执行一些需要用到更新后的 DOM 元素的操作。
  3. 其他需要注意的地方
    • 条件渲染中的 refs:如果使用条件渲染,记得检查某个 ref 是否已经存在,不要直接假设它总存在。
    • 性能考量:尽量减少对浏览器的直接 DOM 操作,利用 React 提供的机制来提高性能和维护性。

总结一下,不建议在 render 中访问 refs,而是在生命周期或其他合适的地方,因为这不仅避免了潜在的错误,还可以使代码更加清晰和可维护。

1.24 childContextTypes 是什么

React 的 childContextTypes 是一个用于声明上下文(context)对象的子类型的属性,它可以让父组件向子组件传递数据,而无需通过层层传递 prop。 childContextTypesgetChildContext 方法一起使用,父组件通过定义这两个属性,将一些数据放在 context 上,然后子组件可以通过 context 对象访问这些数据。

主要作用有两个:

  • 1)跨层级组件通信。
  • 2)提升代码复用性。

这样父组件和深层次的子组件间可以共享一些全局数据,比如主题(theme)、当前用户信息、语言设置等,而无需逐层传递 props。

其实,这部分内容在 React 的新文档中已经不建议继续使用了,因为它存在一些局限性和隐患,比如:改变 context 的值不会自动触发子组件的重新渲染。因此 React 推出了新的 Context API,来替代传统的上下文机制,它更加简单和合理。

1.25 为什么 props 被认为是只读的

React 中的 props 被认为是只读的,因为这有助于确保组件的纯函数性质,使其只依赖于输入的 props 和自身的状态来生成 UI,而不去修改 props 的值。这样可以提高代码的可预测性和可维护性,避免对外部状态的意外更改,从而帮助开发者创建出更加稳定和可调试的应用。

  1. 纯函数和不可变性: React 追求函数式编程风格,其中纯函数是非常重要的一部分。一个纯函数的输出只依赖于输入参数,而且不修改输入参数的值。props 的不可变性确保了父组件和子组件之间的数据传递是单向的,从而避免了副作用和复杂的状态管理。
  2. 单向数据流: React 强调单向数据流,在父组件往子组件传递数据时,通过 props 实现的。这种单向数据流能让数据的变化流程更加清晰和可控。父组件在任何时候都可以信赖其子组件不会改变传递下去的 props
  3. 状态管理: 在 React 中,有一种明确的区分:props 是外部传入的,不可变的;而 state 是组件内部维护的,是可变的。通过这种分离,React 能有效地管理组件状态和属性,增强代码的清晰度。
  4. 性能优化: 因为 props 是不可变的,React 可以在比较 props 是否变化的时候简单地用引用比较来提高性能。如果 props 是可变的,这种引用比较将无法进行,从而需要深度比较,这会增加性能开销。
  5. 调试和测试: 不可变的 props 极大地简化了调试和测试工作。因为 props 保持不变,开发者可以更容易地预测组件的行为,编写测试用例和进行调试。
  6. 特殊情况:函数和对象作为 props: 虽然 props 是只读的,但有时候需要传递函数或对象引用做为 props。此时需要格外小心避免函数或对象内部的副作用。例如,可以使用不可变数据结构或开始使用像 Redux 这样的状态管理工具来进一步维护状态和逻辑的独立性。
  7. 设计模式:不可变数据: 这个思想不仅适用于 React,它是不可变数据设计模式的一部分。许多现代前端框架和库,如 Redux、Immutable.js 等,都推崇不可变数据以提升性能和代码的可维护性。

1.26 React 中如何监听 state 的变化

在 React 中,通常监听 state 的变化是通过在组件内使用生命周期方法(如 componentDidUpdate)或使用 useEffect 钩子来实现的。

1.27 React 中 key 的作用

在 React 中,key 的主要作用是帮助 React 识别哪些元素发生了变化,从而高效地更新 DOM。

在 React 中,更新 UI 时需要比较新旧虚拟 DOM 树之间的差异。key 用于唯一标识每个列表中的元素,使得 React 在更新过程中能准确判断和处理每个元素的变化。

当我们在 React 中渲染一个列表时,每个元素应该都有一个独特的 key 属性。key 帮助 React 更智能地进行差异化比较,避免不必要的 DOM 操作,从而提升渲染性能。没有 key 或者 key 不唯一,可能导致一些潜在的性能问题或者渲染错误。

1.28 React 中构建组件的方式

在 React 中,主要有三种构建组件的方式:函数组件、类组件和纯组件。每种方式都有其特点和适用场景。

  1. 函数组件(Functional Components):这是构建组件的简单方式。函数组件就是 JavaScript 函数,通过接收 props 作为参数并返回一个 React 元素来定义组件。
  2. 类组件(Class Components):这是一个更强大但也更复杂的方式。类组件需要继承自 React.Component,并且至少需要定义一个 render 方法,来返回 React 元素。类组件还可以使用生命周期方法来控制组件的行为。
  3. 纯组件(Pure Components):纯组件也是类组件的一种,但它是通过继承 React.PureComponent 实现的。纯组件自动实现了 shouldComponentUpdate 方法,优化了性能,避免了不必要的渲染。

1.29 PureComponent 下引用类型修改值页面不渲染

解决在 React 的 PureComponent 下引用类型修改值时页面不渲染的问题,最重要的是理解 PureComponent 如何决定是否重新渲染。 PureComponent 通过浅比较(shallow comparison)来判断 props 和 state 是否改变,若改变则重新渲染。如果我们直接修改引用类型(如对象或数组)的值,这些修改不会导致新的引用,从而无法触发重新渲染。因此,解决方案是在修改引用类型时,创建新的引用

具体的解决方案有:

  • 1)对于对象类型的 state 或 props,使用 Object.assign 或者解构赋值来创建新对象。
  • 2)对于数组类型的 state 或 props,可以使用 concat、slice 或者展开展开运算符 ... 来创建新数组。

2. React 拓展知识

2.1 函数式编程是什么?有哪些应用场景?

函数式编程(Functional Programming,简称 FP)是一种编程范式,它将计算机程序视为数学中的函数计算,强调函数的输入输出是确定的,不会受到外部状态的影响。函数式编程一般采用无副作用、不可变数据等特性,使得代码更加模块化、可复用、易于测试和推理等。

函数式编程中的函数是一等公民,即可以像其他类型的数据一样被传递、赋值、返回等。函数式编程也强调函数的高阶抽象能力,通过组合和柯里化等技巧实现复杂的功能。

函数式编程适用于需要处理大量数据的场景,例如数据分析、机器学习、图像处理等。在这些场景中,函数式编程的无副作用、不可变数据等特性可以提高程序的性能和稳定性。函数式编程还适用于需要编写高度可复用的代码库、需要构建高层次抽象的系统等场景。

除此之外,函数式编程还可以用于实现异步编程、事件驱动编程、响应式编程等,它们都是函数式编程的应用场景。例如在 JavaScript 中,Promise 和 RxJS 都是基于函数式编程的思想实现的。

总之,函数式编程是一种强调函数抽象、模块化、不可变性等特性的编程范式,适用于处理大量数据、实现高度可复用代码库、构建高层次抽象的系统等场景。

2.2 函数式编程中的无副作用

函数式编程的一个核心概念是无副作用(Referential Transparency),也就是函数的执行只依赖于输入参数,不会对外部环境产生影响。副作用指的是函数执行过程中对外部环境进行了修改或产生了其他可观察的行为,比如修改全局变量、写入文件、发送网络请求等。

函数式编程强调无副作用的好处在于:

  1. 可靠性:由于函数的结果仅取决于输入参数,相同的输入永远得到相同的输出,不会受到外部环境的变化影响,因此更容易推断和验证函数的行为。
  2. 可测试性:函数无副作用意味着可以更容易地进行单元测试,只需关注输入和输出,而无需关心函数内部的状态变化。
  3. 可组合性:无副作用的函数更容易进行组合,通过将多个函数连接在一起,形成更复杂的功能,而不需要担心它们之间的相互影响。

然而,并非所有的程序都可以完全避免副作用,因为现实世界中很多任务都需要与外部环境进行交互。在函数式编程中,通常会将具有副作用的操作封装在纯函数的边界之外,以保持主要的业务逻辑的纯洁性。

函数式编程并不是要完全消除副作用,而是通过限制和管理副作用的方式来提高程序的可靠性、可测试性和可组合性。

2.3 React 应用的生产环境中,怎么定位到具体错误代码行数

在 React 应用的生产环境中,要定位具体的错误代码行数,可以采取以下几种方法:

  1. 使用 Source Map
    • 在构建生产版本时生成 Source Map 文件。Source Map 是一个映射文件,可以将压缩后的代码映射回原始的开发代码,包括行数、列数等信息。
    • 当生产环境中出现错误时,浏览器会根据 Source Map 将错误定位到原始代码的行数和列数,从而帮助你准确定位问题所在。
  2. 错误边界(Error Boundary)
    • 在 React 应用中使用错误边界来捕获并处理组件中的错误。你可以在错误边界的 componentDidCatch 方法中记录错误信息,包括错误的堆栈信息。
    • 在错误信息中包含堆栈信息,可以帮助你定位到具体出错的组件和代码行数。
  3. 使用监控和日志工具
    • 集成监控和日志工具,如 Sentry、Bugsnag 等,这些工具可以捕获前端错误,并提供详细的错误信息,包括出错的文件、行数等。
    • 通过这些工具提供的错误报告,你可以快速定位到具体的代码行数,并了解错误发生的上下文信息。
  4. 手动添加调试信息
    • 在代码中添加额外的调试信息,例如使用 console.log 输出变量的值或标识代码执行到了哪个阶段。
    • 这种方法虽然相对简单,但需要手动添加代码,并且可能会影响代码的性能和可读性。
  5. 使用浏览器开发者工具
    • 在浏览器的开发者工具中,可以查看 JavaScript 控制台中的错误信息,并通过点击错误信息来跳转到具体的代码行数。
    • 这种方法适用于快速定位到错误发生的地方,但可能无法精确定位到原始的开发代码行数,特别是在代码经过压缩和混淆的情况下。

综上所述,结合使用 Source Map、错误边界、监控工具和浏览器开发者工具,可以帮助你在 React 应用的生产环境中准确定位到具体的错误代码行数。

2.4 React 应用中实现页面切换时保存数据不丢失并记忆滚动条位置

在 React 应用中实现页面切换时保存数据不丢失并记忆滚动条位置,可以采取以下几种优化策略,以提升用户体验并减少页面闪烁:

1、使用 React Router 中的 <Route> 组件的 key 属性:

<Route> 组件中设置唯一的 key 属性,使得当路由切换时,React 不会重新渲染相同的组件,而是复用已经挂载过的组件,从而保留组件的状态和数据。

jsx
<Route key={location.pathname} path="/" exact component={Home} />
<Route key={location.pathname} path="/about" component={About} />

2、在组件中使用 React.memo 进行优化:

对于纯函数组件,可以使用 React.memo 进行包裹,以避免不必要的重新渲染。这样可以确保页面切换时只有必要的组件会重新渲染,减少页面闪烁。

jsx
const MemoizedComponent = React.memo(MyComponent);

3、使用 window.sessionStoragewindow.localStorage 进行数据存储:

在组件卸载前将需要保存的数据存储到 sessionStoragelocalStorage 中,在组件重新挂载时再从中恢复数据。这样可以确保数据不会在页面切换时丢失。

4、记忆滚动条位置:

  • 在页面切换时,监听 windowscroll 事件,将滚动条位置保存到状态或 sessionStorage 中。
  • 在页面重新加载或切换回来时,从状态或 sessionStorage 中恢复滚动条位置,使得用户可以无缝地恢复到之前的滚动位置。

5、使用 React 的 useEffect 钩子进行数据保存和恢复:

在组件中使用 useEffect 钩子监听组件的卸载和挂载事件,在卸载前将需要保存的数据存储起来,在挂载时再从存储中恢复数据。

jsx
useEffect(() => {
  // 在组件卸载前保存数据
  return () => {
    // 在组件重新挂载时恢复数据
  };
}, []);

通过以上优化策略,可以在页面切换时保持数据不丢失并记忆滚动条位置,同时减少页面闪烁,提升用户体验。

2.5 React 中的性能优化有哪些常见的技巧

2.6 React 的代码编写规范

React 代码编写规范主要包括以下几个方面:

  1. 组件命名:组件名应该采用大驼峰命名法(PascalCase),如MyComponent,方便区分普通的HTML元素与React 组件。
  2. 文件结构:遵循组织良好的文件结构,通常每个组件一个文件,并且文件名与组件名保持一致,例如:MyComponent.js
  3. JSX 语法:JSX 中嵌入 JavaScript 代码时,使用小括号包裹代码。避免使用纯 JavaScript 拼接字串形成的方式。保持 JSX 清晰可读。
  4. 状态和属性:避免在组件中直接修改 state 和 props,应该使用setState方法来更新 state,props 是只读的,不能修改。
  5. 无状态组件:对于仅依赖 props 渲染的组件,使用函数组件而不是类组件,因为函数组件更轻量且易于测试。
  6. PropTypes:对组件的 props 进行类型检查,使用PropTypes确保传递到组件的属性类型正确,有助于提高代码的健壮性和可维护性。
  7. 事件处理:事件处理函数应命名为动词短语,例如:handleClickonSubmit,并且应使用箭头函数或者bind绑定this上下文。
  8. 避免重复代码:提取公共逻辑和样式到单独的函数或组件中,遵循DRY(Don't Repeat Yourself)的原则。
  9. Linting:使用 ESLint 标准规范化代码风格,通过 linting 工具确保代码的一致性和质量。

2.7 SSR 和 RSC 的区别

2.8 在 React 中如何进行静态类型检测

在 React 项目中进行静态类型检测的最佳实践是使用 TypeScript。TypeScript 是 JavaScript 的超集,它为 JavaScript 添加了静态类型检查机制。通过使用 TypeScript,你可以提前捕获许多类型相关的错误,从而提高代码的可靠性和可维护性。

  1. 安装 TypeScript 及相关依赖

    bash
    npm install typescript @types/react @types/react-dom
  2. 创建 tsconfig.json

    在项目根目录下创建一个 tsconfig.json 文件,用于配置 TypeScript 编译器选项。一个基本配置如下:

    json
    {
      "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "jsx": "react",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true
      },
      "include": ["src"]
    }
  3. 重命名文件扩展名

    将你的 React 组件和其他文件从 .js / .jsx 扩展名改为 .ts / .tsx

  4. 添加类型注解

    在你的 React 组件及其他代码中逐步添加类型注解。比如,你可以在函数组件中添加类型注解:

    tsx
    import React from 'react';
    
    interface Props {
      name: string;
      age: number;
    }
    
    const Greeting: React.FC<Props> = ({ name, age }) => {
      return (
        <div>
          Hello, my name is {name} and I am {age} years old.
        </div>
      );
    };
    
    export default Greeting;
  5. 配置 ESLint 和 Prettier

    配置 ESLint 和 Prettier 可以进一步保证代码风格和质量一致。你可以安装 eslint-config-airbnb-typescript 来提供一组默认规则:

    bash
    npm install eslint eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-airbnb-typescript

    创建 .eslintrc.js.eslintrc.json 文件,并进行相关配置:

    javascript
    module.exports = {
      parser: '@typescript-eslint/parser',
      extends: [
        'airbnb-typescript',
        'plugin:@typescript-eslint/recommended',
        'plugin:react/recommended',
        'plugin:prettier/recommended',
      ],
      parserOptions: {
        project: './tsconfig.json',
      },
      rules: {
        // 可以根据需要自定义 ESLint 规则
      },
    };
  6. 类型定义文件

    除了 @types/react@types/react-dom ,你还可能需要安装其他类型定义文件来支持你项目中使用的各种库,例如 react-router-dom 的类型定义文件:

    bash
    npm install @types/react-router-dom

2.9 如果 React 组件的属性没有传值,它的默认值是什么

如果一个 React 组件的属性没有传值,它的默认值是 undefined。这意味着组件内部如果直接访问这个属性会得到 undefined

  1. 设定默认属性值:defaultProps

    javascript
    // 类组件
    class MyComponent extends React.Component {
      render() {
        return <div>{this.props.text}</div>;
      }
    }
    MyComponent.defaultProps = {
      text: '默认文本'
    };
    
    // 函数式组件
    const MyComponent = ({ text }) => {
      return <div>{text}</div>;
    };
    MyComponent.defaultProps = {
      text: '默认文本'
    };
  2. 使用 ES6 解构赋值设定默认值

    javascript
    // 函数式组件
    const MyComponent = ({ text = '默认文本' }) => {
      return <div>{text}</div>;
    };
  3. PropTypes 校验

    javascript
    MyComponent.propTypes = {
      text: PropTypes.string
    };

    PropTypes 用于检查传入组件的属性类型是否与预期的类型匹配,有助于捕获传值阶段的潜在 BUG。

2.10 PropTypes 和 Flow 的区别

在 React 中,PropTypes 和 Flow 都是用于类型检查的工具,但它们的使用场景和功能略有不同。简要来说,PropTypes 是一种运行时的类型检查工具,而 Flow 则是一种静态类型检查工具。

  1. PropTypes
    • 运行时检查组件 props 的类型,确保传入的 props 符合预期。
    • 运行时检查:由于 PropTypes 进行的是运行时检查,只有在组件实际运行时才能检测出类型错误。这意味着类型错误的检测依赖于完整的测试覆盖率。
    • 易于配置:PropTypes 是 React 内置支持的,只需引入 prop-types 包并配置相应的类型声明即可。
    • 灵活性:PropTypes 可以对某些复杂的数据结构进行自定义验证,比如数组中的对象类型。
  2. Flow
    • 编译时进行类型检查,帮助开发者在编写代码时就能发现类型错误。
    • 编译时检查:Flow 是编译时进行类型检查的工具,因此在编写代码时就可以发现大部分类型错误,提升了开发效率。
    • 类型推断:相比 PropTypes 的显式类型声明,Flow 可以对一些类型进行自动推断,减少了手动类型声明的工作量。
    • 静态类型:Flow 提供了静态类型系统,可以支持整个项目的类型检查,而不仅仅是组件的 props。
  3. 如何选择:
    • 如果项目相对简单、团队规模较小或者你不想引入额外的构建工具,PropTypes 可能是更好的选择。
    • 如果项目复杂、代码基数较大、团队多人协作且对类型安全性有较高需求,Flow 则会带来更显著的优势。

2.11 如何定时更新一个 React 组件

要定时更新一个 React 组件,你可以使用 JavaScript 的 setInterval 方法结合 React 的生命周期方法(例如 useEffect)来实现。如果使用的是类组件,则可以使用 componentDidMountcomponentWillUnmount 来进行配置和清理定时器。简单来说,通过设置一个定时器,每隔固定的时间间隔去改变组件的 state,从而触发 re-render。

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

const TimerComponent = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);

    return () => clearInterval(interval); // 清理定时器
  }, []);

  return (
    <div>
      <h1>Count: {count}</h1>
    </div>
  );
};

export default TimerComponent;

3. React Hooks

3.1 Hooks 中为什么不能写判断

  1. 挂载阶段:
    • 创建 Hooks 链表。
    • 执行链表。
  2. 更新阶段:
    • 执行链表 --> 校验是否可以执行。

3.2 useState

useState是React中的一个钩子(Hook),它允许我们在函数组件中引入状态(state)。useState返回一个数组,其中第一个元素是当前状态值,第二个元素是更新状态的函数。

使用useState来动态地管理和更新组件的状态。这样可以使我们的组件更加灵活和互动。

3.3 useEffect 和 useLayoutEffect 的区别

  • useEffect:DOM 更新前调用,微任务调用,页面渲染之后执行。适用于:纯数据处理。
  • useLayoutEffect:DOM 更新后调用,同步调用,页面渲染之前执行。适用于:数据修改之后要操作 DOM,防止重复渲染。

注意:DOM 更新 --> 页面渲染(顺序)。

3.4 useRef 和 ref 的区别

useRefref 在 React 中都是用于访问 DOM 元素或组件实例的工具,但它们在使用方式、适用场景以及实现原理上存在明显的区别。以下是关于 useRefref 的详细区别:

  1. 使用方式:
    • ref:在类组件中,通常使用 React.createRef() 方法来创建一个 ref 对象,并将其赋值给类的一个实例属性。然后,可以通过这个 ref 属性来访问对应的 DOM 元素或组件实例。在函数组件中,ref 不能直接使用,因为函数组件没有实例。
    • useRef:是 React 提供的一个 Hook,专门用于函数组件中。它返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(通常是 null)。这个 ref 对象在整个组件的生命周期内保持不变,可以用来保存任何可变的值,不仅仅是 DOM 元素或组件实例。
  2. 适用场景:
    • ref:主要用于类组件中,通过 ref 可以访问到子组件的实例或 DOM 元素,从而进行一些需要直接操作 DOM 或组件实例的操作,如读取输入值、设置焦点等。
    • useRef:在函数组件中替代了 ref 的功能,并且功能更为强大。除了用于访问 DOM 元素或组件实例外,useRef 还可以用来保存一些不需要触发组件重新渲染的状态,如定时器、上一次的渲染结果等。此外,useRef 还可以用来绕过闭包问题,确保回调函数或定时器能够获取到最新的变量值。
  3. 实现原理:
    • ref:在类组件中,ref 是通过实例属性来实现的。在创建组件实例时,React 会将 ref 属性所指向的回调函数或 createRef() 创建的 ref 对象存储到组件实例上,从而可以在组件内部通过 this.refs 或直接访问 ref 对象来访问到对应的 DOM 元素或组件实例。
    • useRef:在函数组件中,由于没有实例,React 通过 useRef Hook 来实现 ref 的功能。useRef 返回一个可变的 ref 对象,这个对象在组件的整个生命周期内保持不变。当组件重新渲染时,useRef 会返回同一个 ref 对象,从而避免了不必要的重新创建和重新赋值。
  4. 注意事项:
    • 在使用 ref 时,需要注意不要在渲染过程中读取或修改 ref 的值,因为这可能会导致不可预测的结果。
    • useRef 返回一个可变的 ref 对象,但不应该在渲染过程中读取或修改 ref 的 .current 属性。通常,我们只在事件处理函数、生命周期方法或副作用(如 useEffect)中读取或修改 ref 的 .current 属性。

总结来说,useRefref 都是用于访问 DOM 元素或组件实例的工具,但它们在使用方式、适用场景以及实现原理上存在差异。在函数组件中,我们通常使用 useRef 来替代 ref 的功能。

3.5 useCallBack 和 useMemo 的区别

useCallbackuseMemo 是 React Hooks 中用于性能优化的两个重要工具。它们的主要区别在于用途、参数、返回值类型以及使用场景。以下是对两者区别的详细解释:

  1. 功能不同:

    • useCallback:用于记忆化(memoize)回调函数,避免在每次渲染时都重新创建相同的函数。它接受一个回调函数和一个依赖项数组作为参数,当依赖项发生变化时,会返回一个新的记忆化的回调函数。
    • useMemo:用于记忆化(memoize)计算结果,避免在每次渲染时都进行昂贵的计算。它接受一个计算函数和一个依赖项数组作为参数,当依赖项发生变化时,会重新计算并返回一个新的记忆化的计算结果。
  2. 参数不同:

    • useCallback:接受一个回调函数和一个依赖项数组作为参数。
    • useMemo:接受一个计算函数和一个依赖项数组作为参数。
  3. 返回值类型不同:

    • useCallback:返回一个记忆化的回调函数。
    • useMemo:返回一个记忆化的计算结果。
  4. 使用场景不同:

    • useCallback:主要用于优化传递给子组件的回调函数,特别是当这些回调函数作为 props 传递,并且父组件频繁渲染时。通过使用 useCallback,可以确保子组件在 props 没有实际变化时不会重新渲染,从而提高性能。
    • useMemo:主要用于优化计算操作,特别是当这些计算操作涉及复杂的计算或数据转换,并且这些计算的结果在多次渲染之间不会发生变化时。通过使用 useMemo,可以避免在每次渲染时都执行昂贵的计算,从而提高性能。
  5. 示例

    • 使用 useCallback 优化回调函数:

      jsx
      const handleClick = useCallback(() => {  
        console.log('Button clicked!');  
      }, []); // 依赖项数组为空,表示没有依赖项
    • 使用 useMemo 优化计算结果:

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

总结来说,useCallbackuseMemo 都是用于性能优化的 React Hooks,但它们的用途、参数、返回值类型和使用场景有所不同。在实际开发中,可以根据具体的需求和场景选择使用哪个 Hook 来提高应用的性能。

4. React 虚拟 DOM

4.1 真实 DOM 和虚拟 DOM 的区别

真实 DOM(Document Object Model)和虚拟 DOM 是前端开发中两个重要的概念,它们的核心区别在于它们的工作原理。

  • 真实 DOM 是浏览器直接操作的 DOM 对象,当你需要更新页面中的某个元素时,浏览器会直接修改该元素的内容,这样的操作是即时的。
  • 虚拟 DOM 是一种抽象形式的 DOM,它是由 React 提出的。虚拟 DOM 是一个 JavaScript 对象,它代表了真实 DOM 的结构变化。在进行更新时,React 首先在虚拟 DOM 中执行,然后对比新旧虚拟 DOM,然后将变化应用到真实 DOM。

4.2 为什么使用虚拟 DOM 来提高性能

React使用虚拟DOM来提高性能是因为直接操作真实DOM是相对昂贵和耗时的。虚拟DOM是一种轻量级的副本,它允许React在内存中进行计算和差异检测,然后将最小的、更高效的变化应用到真正的DOM上。这样可以减少不必要的真实DOM更新,提高应用的整体性能。

  1. 真实DOM的性能瓶颈
    • 浏览器的真实DOM操作(如添加、删除、修改节点)是动画流畅度的天敌。每次操作都要引起浏览器的重排和重绘,这是非常耗时的过程。
    • 当频繁进行这些操作时,会严重影响网页的性能和用户体验。
  2. 虚拟DOM的基本概念
    • 虚拟DOM(Virtual DOM,简称VDOM)是对真实DOM的一种抽象,一般表现为一个轻量级的JavaScript对象,包含元素的属性、事件以及子节点等信息。
    • 有点类似于真实DOM的影子,能在内存中快速生成和更新。
  3. 虚拟DOM的工作方式
    • 在React应用中,当状态改变时,React会首先在内存中的虚拟DOM树计算出新的虚拟DOM。
    • 然后React会对新旧虚拟DOM进行对比,找出有改动的部分,这个过程叫做“diffing”。
    • 最后,React会将差异部分高效地更新到真实DOM,尽量减少对真实DOM的操作,提升性能。
  4. 优势
    • 性能提升: 减少直接操作真实DOM的次数,降低性能开销。
    • 跨平台支持: 虚拟DOM的抽象性使得React不仅可以在浏览器中使用,还能够扩展到移动端(比如React Native),甚至是虚拟现实环境中。
    • 简化开发逻辑: 将开发者从手动管理DOM中解放出来,专注于状态和UI的设置和更新。
  5. 快速例子
    • 假设我们在React组件中更新一个列表项。如果直接操作真实DOM,对列表中的每个项都进行操作需要高昂的计算成本。
    • 使用虚拟DOM,React会生成新的虚拟DOM树,检查所有项发现只有一个需要更新,然后只更新这个项而不影响其他部分。

5. React 底层原理

5.1 Fiber 架构

5.2 MessageChannel

在React中,MessageChannel是一个Web API,它允许我们在不同的浏览上下文(如窗口、iframe或Web Worker)之间建立通信管道。这个API对于React应用来说,虽然不直接用于组件的状态管理或渲染,但在某些特定的异步通信场景中可能会非常有用。以下是对React中异步MessageChannel的详细解释:

  1. MessageChannel的基本概念

    MessageChannel接口创建了一个新的消息通道,这个通道有两个端口(port1port2),它们可以互相发送消息。这些消息是以DOM Event的形式发送的,因此它们是异步的。

  2. MessageChannel在React中的应用场景

    1. 跨窗口或iframe通信
      • 在React应用中,如果需要在主窗口和iframe之间传递消息,MessageChannel可以提供一个安全且高效的方式。
      • 父窗口可以创建一个MessageChannel,并将port2通过postMessage方法传递给iframe。然后,父窗口和iframe就可以通过各自的端口进行双向通信。
    2. Web Worker通信
      • Web Worker是在后台线程中运行JavaScript代码的一种方式,它不会阻塞主线程。
      • 在React应用中,如果需要使用Web Worker来处理耗时任务,MessageChannel可以用于在主线程和Worker之间传递消息。
      • 需要注意的是,由于MessagePort是可传输对象(Transferable Objects),因此它可以在postMessage方法中被传递,从而在Worker中获得并使用。
  3. 使用MessageChannel的注意事项

    1. 安全性
      • 在使用MessageChannel进行跨窗口或iframe通信时,要确保只向受信任的上下文传递端口。
      • 避免在不受信任的上下文中暴露过多的能力,以防止潜在的安全风险。
    2. 资源管理
      • 在通信结束后,应主动调用close方法来断开MessagePort的连接,以便回收资源。
      • 如果不再需要MessageChannel,可以考虑将其删除或置为null,以避免内存泄漏。
    3. 兼容性
      • 尽管MessageChannel在现代浏览器中得到了广泛的支持,但在使用之前仍应检查浏览器的兼容性。
      • 对于不支持MessageChannel的浏览器,可以考虑使用其他通信方式(如localStoragesessionStorage或第三方库)作为替代方案。
  4. 示例代码

    以下是一个简单的示例,展示了如何在React应用中使用MessageChannel进行跨窗口通信:

    javascript
    // 父窗口代码
    const { port1, port2 } = new MessageChannel();
    
    // 监听来自iframe的消息
    port1.onmessage = (event) => {
      console.log('Received message from iframe:', event.data);
    };
    
    // 将port2传递给iframe
    const iframe = document.querySelector('iframe');
    iframe.contentWindow.postMessage('Hello from parent!', '*', [port2]);
    
    // iframe代码(假设在另一个HTML文件中)
    window.onmessage = (event) => {
      if (event.ports.length > 0) {
        const port = event.ports[0];
    
        // 监听来自父窗口的消息
        port.onmessage = (event) => {
          console.log('Received message from parent:', event.data);
    
          // 向父窗口发送消息
          port.postMessage('Hello from iframe!');
        };
      }
    };

    在这个示例中,父窗口创建了一个MessageChannel,并将port2传递给了一个iframe。然后,父窗口和iframe通过各自的端口进行双向通信。

总的来说,MessageChannel在React应用中可以用于处理特定的异步通信场景。然而,对于大多数React应用来说,使用React的状态管理机制(如useStateuseReducer等)和React的上下文(Context)API来处理组件之间的通信可能更为常见和直观。