react在2024年的面试总结

发表于 2024-06-04
更新于 2024-12-19
分类于 技术专栏
阅读量 1025
字数统计 16855

笔者在经历2022年和2024年的常规面试过程中,发现面试官在对于react的面试过程中,存在一些共通性,这里总结一下,可以用来面试前的一个准备。如果深入想要了解react,建议还是读一些更精深的文章。

以下面试记录均基于react16以上,之前的版本已经鲜有人提及了。

1、react16之后的各个版本的一些重大区别

在2024年5月份的面试中,react19已经发布,因此我们在面试的过程中可以把19版本带来的一些更新也提及一下,以表示你时刻关注社区的动态。

1.1、React 16

发布年份:2017

  1. 全新的Fiber架构
    • Fiber架构重新实现了React的内部工作机制,增强了对可中断渲染任务的支持,使得React在处理大型更新时更高效。
  2. 错误边界(Error Boundaries)
    • 通过componentDidCatchstatic getDerivedStateFromError生命周期方法,允许组件在子组件抛出错误时捕获并处理,防止整个应用崩溃。
  3. Fragments
    • 引入<React.Fragment>,允许组件返回多个元素而不需要额外的DOM节点包裹。
  4. Portals
    • 通过ReactDOM.createPortal,可以将子节点渲染到DOM树中的不同位置,适用于模态框、弹出框等场景。
1import React from 'react'; 2import ReactDOM from 'react-dom'; 3import './Modal.css'; // 引入样式文件 4 5const Modal = ({ isOpen, children, onClose }) => { 6 if (!isOpen) return null; 7 8 return ReactDOM.createPortal( 9 <div className="modal-overlay" onClick={onClose}> 10 <div className="modal-content" onClick={e => e.stopPropagation()}> 11 <button className="modal-close" onClick={onClose}> 12 &times; 13 </button> 14 {children} 15 </div> 16 </div>, 17 document.body 18 ); 19}; 20 21export default Modal; 22

1.2、React 17

发布年份:2020

  1. 无破坏性更新

    • React 17注重平滑升级,尽量减少破坏性更改,以便开发者能够轻松从React 16迁移过来。
  2. 事件委托机制的改进

    • 事件处理从document级别移到了根节点,有助于React与非React代码更好地集成,同时提升性能。
      • 为什么这么做?在 React 16 或更早版本中,React 会由于事件委托对大多数事件执行 document.addEventListener()。但是一旦你想要局部使用React,那么React中的事件会影响全局,当把React和jQuery一起使用,那么当点击input的时候,document上和React不相关的事件也会被触发,这符合React的预期,但是并不符合用户的预期。
  3. 更好的版本管理

    • 允许多个React版本共存,支持渐进式升级。
  4. 新JSX转译器

    • 提供了新的JSX转译器,不需要显式地引入React库,更简洁的JSX语法。 以前: 旧的 JSX 转换会把 JSX 转换为 React.createElement(…) 调用。 现在: 新的 JSX 转译器在编译时将 JSX 语法转换为 JavaScript 函数调用。这些函数调用不再依赖全局的 React 变量,而是直接从 React 包中引入所需的功能。

    例如,以下 JSX 代码:

    const element = <h1>Hello, world!</h1>;

    会被编译为:

    1import { jsx as _jsx } from 'react/jsx-runtime'; 2const element = _jsx("h1", { children: "Hello, world!" });

    这种转换方式使得代码更模块化,并且不再依赖全局的 React 变量,从而提升了代码的可维护性和兼容性。

更多细节参考这篇文章:[# 一文解读 React 17 与 React 18 的更新变化](https://segmentfault.com/a/1190000042680491)

1.3、React 18

发布年份:2022

  1. Concurrent Features
    • 引入并发模式(Concurrent Mode),允许React在后台处理多任务渲染,提升应用的响应速度和用户体验。需要使用ReactDOM.createRoot语法才会创建出支持并发的渲染。
  2. 自动批处理
    • 自动批处理更新,减少重渲染次数,提高性能。
    • 这个批处理参考解释我的另外一篇翻译的文章:react18的批处理渲染
  3. Suspense for Data Fetching
    • Suspense组件扩展到数据获取,允许在组件加载数据时显示占位符。
  4. Transition API
    • 通过startTransition API,开发者可以将一些非紧急更新标记为过渡,以优化UI的响应性能。
  5. 改进的SSR
    • 引入流式Hydration和增量式Hydration,进一步提升SSR性能。
  6. React Server Components
    • 引入React Server Components,这是一种新的服务器渲染机制,可以生成不需要客户端JavaScript的组件,提高性能。

React18的改动,掘金上有一篇写的很完整的文章:# React18 新特性解读 & 完整版升级指南

1.4、React 19

发布年份:2024

  1. React编译器:React设计了全新的编译器,最明显的就是以后不用再使用useMemo或者useCallback这两个Hook,编译器会自动帮助你识别是否需要缓存。
  2. 引入Actions:设计了Actions,添加在过渡中使用异步函数的支持,以自动处理待定状态、错误、表单和乐观更新
  3. 原生Metadata:可以在React中更便捷地使用原生的一些meta、script之类的标签了
  4. 资源加载:允许资源在后台加载。
  5. Web Components:提高了与标准的Web Component的兼容性。
  6. 增加了一些新的Hooks:useOptimisticuseFormStatususeFormStateuse

更多细节参考:

1.5、主要区别总结

  • React 16:引入了全新的Fiber架构、错误边界、Fragments、Portals和更快的服务端渲染。
  • React 17:主要关注无破坏性更新、事件处理机制改进、支持多个版本共存和新的JSX转译器。
  • React 18:带来了并发特性、自动批处理、扩展的Suspense、Transition API、改进的SSR和React Server Components。
  • React19:算是一个不大不小的升级,主要聚焦在开发用户体验上,比如更好的服务端组件、更好的报错提示、新的Hooks等

2、react的key属性

面试官会问你为啥需要这个key属性,对于性能有正向的作用吗?

key属性目前是在一些批量渲染的元素中,用来区分兄弟元素的。

  • key只在重新渲染时生效
  • key一致时,React不会移除,重新挂载DOM。也就不会触发组建的render和mounted事件。同时组件内部维护的state也能维持。只会根据props更新DOM的显示内容
  • key不一致时,React会移除组件。重新挂载DOM。所有组件内部state会被重置。

简而言之,如果key属性存在,React在重新渲染过程中使用它来标识相同类型的元素,以便在其兄弟元素中进行区分。

在重新渲染过程中,简化的算法如下:

  • 首先,React会生成元素的“before”和“after”状态的“快照”。
  • 其次,它将尝试识别那些已经存在于页面上的元素,以便在不需要重新创建的情况下重新使用它们。
    • 如果“key”属性存在,React会认为在“before”和“after”两个状态下,具有相同Key属性的项是相同的项。
    • 如果“key”属性不存在,它将使用索引作为默认的“key”。
  • 然后,它会执行以下操作:
    • 删除在“before”阶段存在但在“after”中不存在的项(即卸载它们)。
    • 从头开始创建在“before”中不存在的项(即挂载它们)。
    • 更新在“before”中存在并在“after”中继续存在的项(即重新渲染它们)。

因此key属性对应渲染性能是有影响的。

3、fiber架构的介绍

fiber的架构介绍的文章网上非常多,但是应付面试的话,里面很多细节不用抠得那么细,我这里列举会被常问到的一些问题

3.1、为甚需要fiber?

对于大型项目,组件树会很大,这个时候递归遍历的成本就会很高,会造成主线程被持续占用,结果就是主线程上的布局、动画等周期性任务就无法立即得到处理,造成视觉上的卡顿,影响用户体验。

因此fiber的设计目标就是:

1、任务分解的意义:渲染更新拆分成多次

2、增量渲染(把渲染任务拆分成块,匀到多帧)

3、更新时能够暂停,终止,复用渲染任务

4、给不同类型的更新赋予优先级

5、并发方面新的基础能力

6、更流畅

加上fiber (目的是为了解决渲染可中断)之后,当某个组件发生更新的时候,整体流程:

  1. 触发更新

当组件的状态或属性发生变化时,React 会调度一次更新。更新可以由用户交互、网络响应或计时器等事件触发。

  1. Fiber 树和虚拟 DOM (VDOM)

React 使用 Fiber 树来表示 UI 在任何给定时间点的状态。Fiber 树中的每个节点对应一个 React 元素。VDOM 是一种抽象,允许 React 通过比较新旧 VDOM 来高效地应用更新(这一过程称为协调)。

  1. 协调

当组件更新时:

  • 渲染阶段:React 通过重新渲染组件及其子组件创建新的 VDOM 树,并将其与之前的 VDOM 进行比较以确定哪些部分发生了变化。
  • Diff 算法:React 使用高效的 Diff 算法来识别新旧 VDOM 之间的变化。该算法的时间复杂度为 O(n),即使对于大型树结构也能保持高效。
  1. Fiber 架构

Fiber 架构将渲染工作分解为称为 fibers 的工作单元。每个 fiber 代表 UI 的一部分。Fiber 树允许 React 暂停、优先处理和重用工作单元,从而实现更响应式的更新。

  • 工作单元:每个更新被分解为小的工作单元,React 逐步处理这些单元,有助于保持 UI 的响应性。
  • 并发模式:在并发模式下,React 可以中断低优先级的更新工作,以处理更紧急的更新,从而改善用户体验。
  1. 提交阶段

一旦 React 确定了变化,它就进入提交阶段:

  • 变更前阶段:React 可以在变更实际 DOM 之前运行生命周期方法(如 getSnapshotBeforeUpdate)并执行副作用。
  • 变更阶段:React 更新实际的 DOM,应用在协调阶段识别的更改。
  • 布局效果阶段:React 运行通过 useLayoutEffect 注册的效果以及生命周期方法(如 componentDidMountcomponentDidUpdate)。
  1. 更新 DOM

在变更阶段,React 批量处理 DOM 更新,以最小化重排和重绘,确保最佳性能。更改以最有效的方式应用,从而减少所需的操作数量。

3.2、时间切片

Fiber是React 中的一个核心数据结构,它代表了一个工作单元(即一个组件)。每个 Fiber 对象都有指向其父节点、兄弟节点和子节点的链接,这使得 React 可以在 Fiber 树中自由地导航。(见下一小节的图片示意)

这种数据结构使得 React 可以实现异步和可中断的渲染过程。当 React 开始渲染一个新的更新时,它会从根 Fiber 开始,然后遍历 Fiber 树,为每个 Fiber 创建一个或多个新的 Fiber 来表示新的状态。这个过程被称为“reconciliation”(调和)。

在 reconciliation 过程中,React 可以根据每个 Fiber 的优先级来决定是否立即处理它,或者将它推迟到稍后处理。如果有一个更高优先级的任务(如用户交互)出现,React 可以中断当前的工作,去处理那个任务,然后在有空闲时间时再回来继续之前的工作。这就是所谓的时间切片

时间分片旨在让应用在 CPU 进行大量计算时也能与用户交互,但时间分片只能对大量 CPU 计算进行优化,无法优化复杂 DOM 操作, 因为要确保用户正在操作的界面是最新的

react fiber是通过requestIdleCallback这个api去控制的组件渲染的“进度条”。

requesetIdleCallback是一个属于宏任务的回调,就像setTimeout一样。不同的是,setTimeout的执行时机由我们传入的回调时间去控制,requesetIdleCallback会在事件循环空闲时调用回调函数。当在callback中递归调用requesetIdleCallback时,又会等到事件循环队列为空时才执行回调。让用户的事件得到响应。

它的回调函数可以获取本次可以执行的时间,每一个16ms除了requesetIdleCallback的回调之外,还有其他工作,所以能使用的时间是不确定的,但只要时间到了,就会停下节点的遍历。

requestIdleCallback的回调函数可以通过传入的参数deadLine.timeRemaining()检查当下还有多少时间供自己使用。(但由于兼容性不好,加上该回调函数被调用的频率太低,react实际使用的是一个polyfill(自己实现的api),而不是requestIdleCallback。)

补充一下,没有用requestIdleCallback原因有三点:一是浏览器兼容性,二是执行频率其实不稳定,三是这个API设计初衷是为了低优先级任务。React除了使用messagechanel、setTimeout外,还有setImmediate,主要考虑的是兼容性,还有就是执行时机比messagechanel早

关于为啥使用messageChannel的,可以参考:# MessageChannel在react中的作用?

3.2、fiber树的遍历

fiber树的结构: react-fiber-structure01.png

上述fiber树的遍历顺序: 1)如果存在子节点,先遍历子节点; 2)不存在子节点,存在兄弟节点,遍历下一个兄弟节点; 3)不存在子节点,不存在下一个兄弟节点,遍历父节点; 4)当前节点的所有子节点生成完毕,判断有无下一个兄弟节点。

重复执行上述操作,树的后序遍历,保证所有叶子节点的fiber Node最先被生成。

这里有个动画:

更多细节可以参考:

4、react的事件系统

React为啥需要做自己的事件系统

  • 抹平不同浏览器 API 的差异,更便于跨平台
  • 事件合成可以处理兼容性问题
  • 利用事件委托机制,支持动态绑定,简化了 DOM 事件处理逻辑,减少了内存开销,这句话的解释是react17现在回把所有事件委托放在根容器下(以前的版本是放在body下的),这样只需要监听根容器即可,减少了在实际DOM元素注册事件的内存开销。其次还可以减少每次事件触发之后需要遍历目标元素符合的事件回调,只需要在根元素下查找目标元素满足的事件即可。
  • React 16 正式版本之后引入 Fiber 架构,React 可以通过干预事件的分发以优化用户的交互体验

5、react16之后为啥推崇函数组件

  • React的官方推崇组件是组合的方式,而不是继承的,类组件是面向对象编程,更多使用继承的方式
  • 函数组件是无状态组件,方便服用,并且可以颗粒度小
  • 函数组件做到了“捕获了渲染时所使用的值”,以Dan的那个经典例子为例;
  • 类组件的this指针时刻指向最新的状态变化;
  • 调用方式,函数组件可以直接调用,类组件需要实例化

6、react的useState是异步还是同步?

结论在这里:

react18之前还可能因为调用的地方不同(区分是在合成事件、原生事件还是setTimeout/Promise这种异步函数中,合成事件里调用setState都是异步的,并且会进行批量更新,后两个都是同步的,没执行一次就触发一次更新),react18之后,因为引入自动批量更新机制,无论在哪里调用,全部都是异步的。

# React 的 setState 是同步还是异步?

7、useEffect是一定在页面渲染后才会执行吗?

结论在这里: image.png

更多细节请参考:# useEffect 一定在页面渲染后才会执行吗?

8、常用的Hooks

最常用的那几个就不说了:useStateuseEffectuseCallbackuseMemouseRefuseContextuseReducer

8.1、useLayoutEffect

useLayoutEffect 在所有 DOM 变更后同步调用,与 useEffect 的区别是它在浏览器完成绘制之前同步调用(这个其实不严谨的,可以参考第7小节)。并且最新版的卸载回调也是同步调用的。

1import React, { useLayoutEffect, useRef } from 'react'; 2 3function LayoutEffectExample() { 4 const divRef = useRef(null); 5 6 useLayoutEffect(() => { 7 console.log(divRef.current.getBoundingClientRect()); 8 }, []); 9 10 return <div ref={divRef}>Hello, useLayoutEffect</div>; 11} 12

8.2、useImperativeHandle

useImperativeHandle 自定义使用 ref 时暴露给父组件的实例值。

1import React, { forwardRef, useImperativeHandle, useRef } from 'react'; 2 3const FancyInput = forwardRef((props, ref) => { 4 const inputRef = useRef(); 5 6 useImperativeHandle(ref, () => ({ 7 focus: () => { 8 inputRef.current.focus(); 9 } 10 })); 11 12 return <input ref={inputRef} />; 13}); 14 15function Parent() { 16 const inputRef = useRef(); 17 18 return ( 19 <div> 20 <FancyInput ref={inputRef} /> 21 <button onClick={() => inputRef.current.focus()}>Focus</button> 22 </div> 23 ); 24} 25

8.3、useDebugValue

useDebugValue 用于在 React 开发者工具中显示自定义 Hook 的标签。

1import React, { useDebugValue, useState } from 'react'; 2 3function useFriendStatus(friendID) { 4 const [isOnline, setIsOnline] = useState(null); 5 6 // 你可以在这里设置调试值 7 useDebugValue(isOnline ? 'Online' : 'Offline'); 8 9 return isOnline; 10} 11

8.4、useTransition

useTransition 用于管理 UI 的并发更新(使用了这个Hooks就代表开启了并发更新)。它允许你将某些更新标记为非紧急更新,从而保持 UI 的响应速度。

startTransition,主要为了能在大量的任务下也能保持 UI 响应。这个新的 API 可以通过将特定更新标记为“过渡”来显著改善用户交互,简单来说,就是被 startTransition 回调包裹的 setState 触发的渲染被标记为不紧急渲染,这些渲染可能被其他紧急渲染所抢占。

1import React, { useState, useTransition } from 'react'; 2 3function TransitionComponent() { 4 const [isPending, startTransition] = useTransition(); 5 const [count, setCount] = useState(0); 6 7 const handleClick = () => { 8 startTransition(() => { 9 setCount(count + 1); 10 }); 11 }; 12 13 return ( 14 <div> 15 <button onClick={handleClick}>Increment</button> 16 {isPending ? <p>Loading...</p> : <p>Count: {count}</p>} 17 </div> 18 ); 19} 20

8.5、useDeferredValue

useDeferredValue 用于将一个快速变化的值延迟到稍后更新,从而保持更高的响应性

useTransitionuseDeferredValue类似于useCallbackuseMemo,前者更新任务变成了延迟更新任务,后者产生一个新的值,这个值作为延时状态

1import React, { useState, useDeferredValue } from 'react'; 2 3function DeferredValueComponent() { 4 const [value, setValue] = useState(''); 5 const deferredValue = useDeferredValue(value); 6 7 return ( 8 <div> 9 <input value={value} onChange={e => setValue(e.target.value)} /> 10 <p>Deferred Value: {deferredValue}</p> 11 </div> 12 ); 13} 14

8.6、useInsertionEffect

这个 Hooks 只建议 css-in-js 库来使用。 这个 Hooks 执行时机在 DOM 生成之后,useLayoutEffect 之前,它的工作原理大致和 useLayoutEffect 相同,只是此时无法访问 DOM 节点的引用,一般用于提前注入 <style> 脚本。

1const useCSS = rule => { 2 useInsertionEffect(() => { 3 if (!isInserted.has(rule)) { 4 isInserted.add(rule); 5 document.head.appendChild(getStyleForRule(rule)); 6 } 7 }); 8 return rule; 9}; 10 11const App: React.FC = () => { 12 const className = useCSS(rule); 13 return <div className={className} />; 14}; 15 16export default App; 17

#### 8.7、useFormState 用于访问上一次表单提交的返回值,可以方便地显示确认消息或错误信息。

感谢网友IsKaros的指正,useFormStatus官方上变更为了useActionState,见8.10小节

8.8、useFormStatus

useFormStatus 提供了一种从表单内部组件获取表单状态的简便方法,包括表单是否正在提交、提交是否成功或失败等。在大型表单中,通过 useFormStatus 可以轻松地在不同组件间共享表单的提交状态。

1import React, { useFormStatus, useFormState } from 'react'; 2 3async function loginUser(formData) { 4 const response = await fetch('/api/login', { 5 method: 'POST', 6 body: JSON.stringify(formData), 7 headers: { 'Content-Type': 'application/json' }, 8 }); 9 10 if (!response.ok) { 11 throw new Error('Login failed'); 12 } 13 14 return response.json(); 15} 16 17function LoginStatus() { 18 const { pending, data, error } = useFormStatus(); 19 20 if (pending) { 21 return <p>Logging in...</p>; 22 } 23 24 if (error) { 25 return <p>Error: {error.message}</p>; 26 } 27 28 if (data) { 29 return <p>Welcome back, {data.username}!</p>; 30 } 31 32 return null; 33} 34 35function LoginForm() { 36 const [state, formAction] = useFormState(loginUser, null); 37 38 return ( 39 <form action={formAction}> 40 <input type="text" name="username" placeholder="Username" required /> 41 <input type="password" name="password" placeholder="Password" required /> 42 <button type="submit">Login</button> 43 <LoginStatus /> 44 </form> 45 ); 46} 47 48export default LoginForm; 49

8.9、useOptimistic

useOptimistic 允许在提交数据到服务器之前,预先更新状态,从而提供更即时的反馈。这个 Hook 可以帮助简化在处理异步操作时的状态管理,特别是在提交操作可能会失败的情况下。

1import React, { useOptimistic } from 'react'; 2 3async function saveTaskToServer(task) { 4 const response = await fetch('/api/tasks', { 5 method: 'POST', 6 body: JSON.stringify(task), 7 headers: { 'Content-Type': 'application/json' }, 8 }); 9 10 if (!response.ok) { 11 throw new Error('Failed to save task'); 12 } 13 14 return response.json(); 15} 16 17function TaskList() { 18 const [tasks, addTaskOptimistically] = useOptimistic([], (currentTasks, newTask) => [ 19 ...currentTasks, 20 newTask, 21 ]); 22 23 const handleAddTask = async (taskName) => { 24 const newTask = { id: Date.now(), name: taskName }; 25 addTaskOptimistically(newTask); 26 27 try { 28 await saveTaskToServer(newTask); 29 } catch (error) { 30 console.error('Failed to save task:', error); 31 } 32 }; 33 34 return ( 35 <div> 36 <ul> 37 {tasks.map(task => ( 38 <li key={task.id}>{task.name}</li> 39 ))} 40 </ul> 41 <button onClick={() => handleAddTask('New Task')}>Add Task</button> 42 </div> 43 ); 44} 45 46export default TaskList; 47

8.10、useActionState

useActionState 使得在组件中处理异步操作变得更加简单和直观。可以方便地处理异步操作中的错误情况,并在组件中显示相应的错误信息。

1import React from 'react'; 2import { useActionState } from 'react'; 3 4async function fetchUserData() { 5 const response = await fetch('/api/user'); 6 if (!response.ok) { 7 throw new Error('Failed to fetch user data'); 8 } 9 return await response.json(); 10} 11 12function UserData() { 13 const [state, loadUserData] = useActionState(fetchUserData); 14 15 return ( 16 <div> 17 {state.pending && <p>Loading user data...</p>} 18 {state.error && <p>Error: {state.error.message}</p>} 19 {state.data && ( 20 <div> 21 <h1>{state.data.name}</h1> 22 <p>{state.data.email}</p> 23 </div> 24 )} 25 <button onClick={loadUserData}>Load User Data</button> 26 </div> 27 ); 28} 29 30export default UserData; 31

9、use

use 是一个 React API,它可以让你读取类似于 Promisecontext 的资源的值。比如当 context 被传递给 use 时,它的工作方式类似于useContext。而 useContext 必须在组件的顶层调用,use 可以在条件语句如 if 和循环如 for 内调用。相比之下,useuseContext更加灵活。

1"use client"; 2 3import { use, Suspense } from "react"; 4 5function Message({ messagePromise }) { 6 const messageContent = use(messagePromise); 7 return <p>Here is the message: {messageContent}</p>; 8} 9 10export function MessageContainer({ messagePromise }) { 11 return ( 12 <Suspense fallback={<p>⌛Downloading message...</p>}> 13 <Message messagePromise={messagePromise} /> 14 </Suspense> 15 ); 16}

10、react的性能和vue对比

在React里经常会出现CPU计算量大的场景,也就是说100毫秒以上的计算量出现的频率很高。
原因:

  1. Fiber 架构的复杂性导致 React 的虚拟 DOM 协调效率较低,这是系统性的问题。
  2. React 使用 jsx的开发方式,甚至完全兼容js的语法特性,导致其动态性过于强,不运行根本不知道vdom其结构是怎样的,是否发生了变化。所以必须在每次更新时都重新运行,做很多的计算以保证结果的正确性, 导致它的渲染效率比 template 低,因为 template 很容易做静态分析和优化。
  3. React Hooks 将大部分组件树的优化 API 暴露给开发者,开发者很多时候需要手动调用 useMemo 来优化渲染效率。这意味着 React 应用默认就有 render 过多的问题。更严重的是,这些优化在 React 里很难自动化,因为这些优化要求开发者正确设置依赖数组,盲目添加 useMemo 会导致应该 render 的没 render, 很不幸,大部分开发者都很懒,不会在每个地方都加上优化,因此大部分 React 应用都会有大量的没必要的 CPU 计算工作。

公众号关注一波~

微信公众号

关于评论和留言

如果对本文 react在2024年的面试总结 的内容有疑问,请在下面的评论系统中留言,谢谢。

网站源码:linxiaowu66 · 豆米的博客

Follow:linxiaowu66 · Github