涂鸦智能-前端校招一面-React、微前端、Rag、Next

0 阅读14分钟

image.png

一、React Hooks 相关(1-14 题)

1. 项目中使用过哪些常用的 hooks?

答:常用的包括:

  • 基础:useStateuseEffectuseRefuseContext
  • 性能优化:useMemouseCallback
  • 状态管理:useReducer
  • React 18 新增:useIduseTransitionuseDeferredValueuseSyncExternalStore
  • 其他:useImperativeHandleuseLayoutEffect

2. useState 为什么返回一个数组的形式?

答:

  1. 解构赋值的灵活性:返回数组可以让用户自由命名状态变量和更新函数,比如 const [count, setCount],如果返回对象,就只能用固定的 key(如 statesetState),无法自定义变量名。
  2. 支持多个状态:同一组件中可以多次调用 useState,分别声明不同状态,互不干扰。
  3. API 设计简洁:相比返回对象,数组解构更直观,代码更简洁。

3. hooks 函数为什么要在顶层使用?

答:React 依赖 hooks 调用顺序 来关联组件的状态。

  • 若在条件、循环、嵌套函数中调用 hooks,会导致调用顺序不固定,React 无法正确匹配状态,出现状态错乱的问题。
  • 规则:不能在 if/for/while 等条件语句、嵌套函数中调用 hooks,必须在组件的顶层调用。

4. setState 做了哪些事情?

这里默认指 React 类组件的 setState,函数组件的 setXxx 底层逻辑类似:

  1. 合并更新(批量更新) :多次 setState 会被合并成一次更新,减少渲染次数。
  2. 触发协调(Diff 算法) :生成新的虚拟 DOM 树,与旧树对比,找出差异。
  3. 更新真实 DOM:将差异应用到真实 DOM,完成页面更新。
  4. 触发生命周期 / Effect:类组件触发 shouldComponentUpdatecomponentDidUpdate,函数组件触发 useEffect

5. React 的 Diff 算法跟 Vue 的 Diff 算法有什么区别?

表格

维度React DiffVue Diff
对比方式同层节点对比,采用双端遍历(旧头、旧尾、新头、新尾),找不到再按 key 找Vue 2 同层双端遍历;Vue 3 优化为最长递增子序列,减少移动操作
key 的作用用于识别节点是否可复用,无 key 会强制删除重建同 React,Vue 3 中 key 对优化移动操作更关键
复杂度优化时间复杂度优化到 O (n)Vue 2 同 React;Vue 3 最长递增子序列算法进一步减少移动次数
场景侧重列表结构简单、移动少的场景列表频繁移动、增删的场景,Vue 3 性能更优

6. 它的 Diff 算法的时间复杂度是什么?

答:React 的 Diff 算法将传统树对比的 O (n³) 复杂度,通过同层对比、key 复用、单向遍历优化到了 O(n) ,其中 n 是节点总数。


7. Vue 3 的 Diff 算法中,最长递增子序列的算法内部实现了解吗?

答:Vue 3 中,当新旧列表节点无法通过双端遍历复用节点时,会用最长递增子序列(LIS)算法找出不需要移动的节点,只移动非递增子序列中的节点,从而减少 DOM 操作。实现思路:

  1. 先建立新节点索引与旧节点索引的映射数组。
  2. 对映射数组求 LIS,得到最长的、顺序不变的子序列。
  3. 遍历新列表,LIS 中的节点保持原位,其他节点移动到正确位置。算法采用贪心 + 二分实现,时间复杂度为 O (n log n)。

8. React 中 useReducer 使用过没?

答:用过。useReduceruseState 的替代方案,适合处理复杂状态逻辑。

  • 语法:const [state, dispatch] = useReducer(reducer, initialState)
  • 适用场景:状态更新逻辑复杂、多个子值相互依赖、需要集中管理状态变更逻辑。
  • 示例:

js

const initialState = { count: 0 };
function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    default: return state;
  }
}
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'increment' });

9. Zustand 为什么只适用于轻量的项目?

答:

  1. 缺乏中间件生态:相比 Redux,Zustand 中间件较少,复杂场景(如日志、持久化、异步流)支持弱。
  2. 状态管理分散:Store 可独立创建,大型项目中容易导致状态碎片化,难以统一管理和调试。
  3. DevTools 支持弱:调试体验不如 Redux DevTools 完善。
  4. 不依赖 Provider 导致的问题:大型项目中难以做统一的状态注入和权限控制。

10. Zustand 为什么不需要依赖 Provider?它的底层实现是怎么样的?

答:

  • 为什么不需要 Provider:Zustand 的 Store 是单例模式,直接创建的 store 实例是全局的,组件直接引用即可获取状态,不需要通过 Context 传递。

  • 底层实现

    1. 核心是 create 函数,创建一个包含状态、更新函数的 store 对象。
    2. store 内部维护一个状态对象和订阅者列表。
    3. useStore hook 内部使用 useSyncExternalStore,订阅 store 的状态变化,状态更新时触发组件重渲染。

11. Zustand 掉了 set 方法,为什么能更新对应的组件,重新 render?

答:Zustand 的 set 方法会触发 store 的状态更新,并通知所有订阅该状态的组件。

  • 组件通过 useSyncExternalStore 订阅 store,状态变化时,useSyncExternalStore 会触发组件重渲染。
  • 本质上,组件订阅的是 store 的状态变化事件,set 方法会发布事件,通知订阅者更新。

12. useSyncExternalStore 底层怎么实现的?为什么能触发 setState 一样的更新机制?

答:

  • 底层实现:

    1. 接收三个参数:subscribe(订阅函数)、getSnapshot(获取状态快照)、getServerSnapshot(可选,服务端渲染用)。
    2. 组件挂载时,调用 subscribe 注册回调,当外部 store 状态变化时,回调会触发。
    3. 回调内部调用 getSnapshot 获取最新状态,对比新旧状态,若不同则触发组件重渲染。
  • 为什么能触发更新:useSyncExternalStore 是 React 官方提供的用于订阅外部 store 的 hook,它会在状态变化时,通过 React 的内部调度机制触发组件更新,和 setState 触发更新的底层调度是一致的。


13. React 18 新增了哪些 Hooks?

答:

  1. useId:生成唯一 ID,解决 SSR 中 ID 不匹配的问题。
  2. useTransition:将状态更新标记为过渡,实现非阻塞更新,优化用户体验。
  3. useDeferredValue:延迟更新非紧急的状态,优先处理高优先级更新。
  4. useSyncExternalStore:订阅外部 store(如 Zustand、Redux)的状态变化。
  5. useInsertionEffect:用于 CSS-in-JS 库,在布局副作用前注入样式。

14. Redux 使用过吗?

答:用过。Redux 是一个集中式状态管理库,核心概念包括:

  • store:唯一的状态容器。
  • action:描述状态变化的对象。
  • reducer:纯函数,接收旧状态和 action,返回新状态。
  • dispatch:发送 action,触发状态更新。现代 Redux 一般用 Redux Toolkit,简化了配置,内置了 immer、异步处理等功能。

15. 对纯函数的理解?

答:纯函数是指满足以下两个条件的函数:

  1. 相同输入,得到相同输出:输出只由输入决定,不受外部状态影响。
  2. 无副作用:不修改外部变量、不进行 I/O 操作、不调用非纯函数等。
  • 示例:const add = (a, b) => a + b;
  • 意义:可预测、可测试、可缓存,是 React 状态管理(如 Redux reducer)的核心要求。

二、微前端相关(16-19 题)

16. 项目中微前端使用了什么架构?

答:我们项目使用的是 qiankun 微前端框架,主应用 + 多个子应用的架构,主应用负责路由分发、公共依赖加载,子应用独立开发、独立部署。


17. 为什么选择 qiankun,而不是 ice(飞冰)的微前端架构?

答:

  1. 生态成熟度:qiankun 基于 single-spa,社区更活跃,文档和问题解决方案更多。
  2. 技术栈无关:qiankun 支持 React、Vue、Angular 等任意技术栈的子应用,而 ice 微前端更偏向阿里体系的技术栈。
  3. 沙箱隔离完善:qiankun 提供了 JS 沙箱和样式沙箱,解决了全局变量污染和样式冲突问题。
  4. 配置简单:qiankun 接入成本低,子应用改造少,适合快速落地。

18. qiankun 和 ice 微前端在性能 / 其他方面有什么不一样?

答:

  • 性能:qiankun 沙箱隔离(尤其是 JS 沙箱)会有一定性能开销,ice 微前端的沙箱优化更好,性能略优。
  • 生态:ice 微前端和 ice 工程体系绑定较深,qiankun 更通用,适配性更强。
  • 场景侧重:ice 微前端更适合阿里内部业务,qiankun 更适合跨技术栈、跨团队的复杂微前端场景。

19. qiankun 里面的沙箱隔离是怎么做的?

答:qiankun 提供了两种沙箱:

  1. JS 沙箱

    • 旧版:基于 with + Proxy,隔离子应用的全局变量,防止污染主应用。
    • 新版(快照沙箱):支持不支持 Proxy 的浏览器,通过记录 / 恢复全局变量快照实现隔离。
  2. 样式沙箱

    • 通过给子应用的样式加前缀(如 data-qiankun 属性),或者使用 shadow DOM,实现样式隔离,防止子应用样式污染主应用。

三、知识库 / AI 检索相关(20-26 题)

20. 知识库的切片方式有哪几种?

答:常见的切片方式:

  1. 固定长度切片:按固定字符数(如 512 字符)切割,实现简单,但可能破坏语义。
  2. 语义切片:按句子、段落、章节等语义单元切割,保留语义完整性,效果更好。
  3. 递归字符切片:按不同层级的分隔符(如换行、句号、空格)递归切割,平衡语义完整性和长度限制。
  4. 基于 LLM 的智能切片:利用大模型识别文本语义边界,智能切割,效果最好但成本高。

21. 切片之后,下面的流程有哪些?

答:

  1. 向量化:将切片后的文本通过 Embedding 模型转换为向量。
  2. 向量存储:将向量和元数据存入向量数据库(如 Pinecone、Chroma)。
  3. 检索阶段:用户查询文本,同样转换为向量,进行相似度匹配。
  4. 结果召回:返回相似度最高的切片文本。
  5. 增强生成:将召回的文本和用户问题一起传入 LLM,生成回答。

22. 检索过程有哪些优化可以做?

答:

  1. 向量数据库优化:使用 HNSW、IVF 等索引算法,加快向量检索速度。
  2. 混合检索:结合向量检索和关键词检索(如 BM25),提高召回率。
  3. 重排序:使用 CrossEncoder 等模型对召回结果进行二次排序,提升相关性。
  4. 查询改写:对用户查询进行扩展、纠错、意图识别,提高匹配准确率。
  5. 缓存优化:对高频查询结果进行缓存,减少重复检索。

23. 项目中的向量维度是多少?

答:(根据实际情况回答,比如)我们项目使用的是 OpenAI 的 text-embedding-ada-002 模型,向量维度是 1536;或者使用国内模型,如智谱的 Embedding 模型,维度是 1024。


24. 相似度匹配的原理是什么?

答:文本被转换为向量后,通过计算向量之间的相似度来衡量文本的相关性,常见的计算方式:

  • 余弦相似度:计算两个向量的夹角余弦值,值越接近 1,相似度越高,是向量检索中最常用的方式。
  • 欧氏距离:计算两个向量的直线距离,距离越小,相似度越高。
  • 点积:计算向量的点积,向量长度归一化后等价于余弦相似度。

25. 如何发起一个 SSE 请求?

答:使用 EventSource API 发起 SSE 请求:

js

const eventSource = new EventSource('/stream');
eventSource.onmessage = (e) => {
  console.log('收到消息:', e.data);
};
eventSource.onerror = (err) => {
  console.error('SSE 错误:', err);
  eventSource.close();
};
// 关闭连接
// eventSource.close();

26. fetch 和 EventSource 实现流式输出的区别是什么?

答:

表格

维度EventSourcefetch
协议基于 HTTP/1.1,只支持 GET 请求支持 HTTP/1.1/2/3,支持 GET/POST 等方法
连接方式自动重连,服务端可发送事件类型需手动处理重连,无内置事件类型
数据格式固定的 text/event-stream 格式自定义格式(如 JSON、纯文本)
浏览器支持所有现代浏览器支持支持度高,可配合 ReadableStream 使用
适用场景服务端主动推送、消息通知大文件下载、LLM 流式输出等复杂场景

四、Next.js/ TS 相关(27-37 题)

27. 项目是小程序还是 H5?

答:(根据实际情况回答,比如)我们项目是 H5 应用,使用 Next.js 开发,支持 SSR/SSG。


28. 为什么选择 Next 15?

答:

  1. App Router 稳定:Next 15 中 App Router 成为默认,支持 React Server Components,减少客户端 JS 体积。
  2. Server Actions:内置服务端交互方案,简化表单提交、数据请求,避免手写 API。
  3. 增量静态再生成(ISR) :支持静态页面的增量更新,兼顾性能和实时性。
  4. 性能优化:内置图片优化、字体优化、路由预加载,提升用户体验。
  5. TypeScript 友好:内置 TS 支持,类型推断完善,开发体验好。

29. 使用 Next 会不会给服务器增加压力?

答:

  • 会有一定压力,但可以通过优化缓解:

    1. 对于静态页面,使用 SSG/ISR 生成静态文件,服务器只需托管静态资源,压力很小。
    2. 对于 SSR 页面,会在服务器渲染 HTML,增加 CPU 压力,但可以通过缓存(如 CDN、Redis)缓解。
    3. Next 支持边缘运行时(Edge Runtime),在 CDN 节点渲染,分散服务器压力。

30. 项目中使用的是 Page Router 还是 App Router?

答:我们项目使用的是 App Router,它是 Next 推荐的路由方案,支持 React Server Components、嵌套布局、加载状态等新特性。


31. Next.js 中文件夹即路由的实现原理?

答:App Router 基于文件系统路由,规则如下:

  1. app/ 目录为根,文件夹结构对应 URL 路径。

  2. 特殊文件:

    • page.tsx:对应路由的页面组件,决定该路径的内容。
    • layout.tsx:对应路由的布局组件,嵌套路由共享布局。
    • loading.tsx:路由加载时的骨架屏组件。
    • error.tsx:路由错误时的错误边界组件。
  3. 动态路由:用 [param] 命名文件夹,对应 URL 中的动态参数。Next.js 会自动扫描 app/ 目录下的文件,生成路由表,映射 URL 到对应的组件。


32. TS 使用过吗?

答:用过,项目中全程使用 TypeScript 开发,用于类型检查、提升代码可维护性和开发体验。


33. interfacetype 的区别是什么?

答:

表格

维度interfacetype
扩展方式extends 继承,支持声明合并& 交叉类型扩展,不支持声明合并
适用场景主要用于对象 / 类的类型定义可定义对象、联合类型、元组、基本类型等
声明合并支持,多个同名 interface 会合并不支持,同名 type 会报错
计算属性不支持支持,如 `type Keys = 'a''b'; type Obj = { [key in Keys]: string }`

34. 如何提取一个函数的参数类型?

答:使用 TypeScript 内置工具类型 Parameters<T>

ts

function add(a: number, b: number): number {
  return a + b;
}
type AddParams = Parameters<typeof add>; // [number, number]

35. 如果要封装一个工具类型,会怎么做?

答:根据需求,使用 TypeScript 的泛型、条件类型、映射类型等特性封装。示例:封装一个提取函数返回类型的工具类型(类似 ReturnType):

ts

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function add(a: number, b: number): number {
  return a + b;
}
type AddReturn = MyReturnType<typeof add>; // number

36. Next.js 中的水合和同构你了解吗?

答:

  • 同构(Isomorphic) :代码可以同时在服务端和客户端运行,Next.js 中服务端渲染 HTML,客户端复用服务端渲染的代码进行交互绑定。

  • 水合(Hydration) :客户端加载 JS 后,将服务端渲染的静态 HTML 转换为可交互的 React 组件的过程。

    1. 服务端渲染生成 HTML 字符串,返回给客户端。
    2. 客户端加载 JS,解析组件,将事件处理函数绑定到 DOM 上,使页面可交互。
    3. 水合过程中,React 会对比服务端渲染的 HTML 和客户端渲染的虚拟 DOM,确保一致,否则会报错。

37. 水合过程中,服务端的数据如何给到客户端?

答:

  1. SSR 场景:服务端渲染时,通过 getServerSideProps(Page Router)或 Server Components 获取数据,渲染到 HTML 中,客户端水合时,Next.js 会将数据序列化后注入到 HTML 的 __NEXT_DATA__ 中,客户端 JS 读取该数据,用于水合。
  2. SSG/ISR 场景:构建时获取数据,生成静态 HTML,数据直接嵌入 HTML 中,客户端直接使用。
  3. App Router:Server Components 直接在服务端获取数据,渲染 HTML,客户端 Client Components 可以通过 props 接收服务端传递的数据,无需额外请求。