面灵AI→

百度前端面试:JSX 与 Babel、SSE 流式输出与 RAG 多路召回

时间
2026-10
来源
牛客网

《面试题目》

  1. JSX 是什么?为什么要用这样的写法?原理是什么?浏览器是如何识别并解析的?
  2. 在低版本浏览器上支持高版本语法,会用什么工具?Babel 解析的原理是什么?和 polyfill 有什么区别?
  3. 浏览器从加载页面到渲染的流程是什么?
  4. 常见 React Hooks 有哪些?
  5. useEffect 的依赖项如果是空数组代表什么意思?
  6. useMemo、useCallback、React.memo 分别缓存什么?什么时候真的有用?
  7. 什么是 Virtual DOM?React 为什么需要它?介绍一下 React 的虚拟 DOM 和 diff 算法。
  8. JSBridge 是什么?H5 怎么调用 Native?Native 又怎么把结果回调给 JS?底层原理了解吗?
  9. WebView 是什么?H5 跑在 App 里时到底发生了什么?
  10. React Native 这类跨端框架是怎么工作的?
  11. HTTP 短轮询、SSE、WebSocket 分别是什么?各自适合什么场景?
  12. SSE 为什么适合 AI 流式输出?怎么做断线补发?
  13. 介绍一下 Webpack 的原理,完整构建流程是什么?
  14. Loader 和 Plugin 有什么区别?分别解决什么问题?
  15. Skills、MCP、A2A 有什么区别?
  16. Harness 工程:如果想让 AI 按指定工作流执行,应该怎么做?
  17. 智能体的上下文你是怎么给的?如果一次性给太多信息怎么处理?Agent 上下文太大怎么办?
  18. Skill 按需加载怎么组合?
  19. SSE 流式事件的作用是什么?还了解其他通信方式吗?
  20. 介绍 RAG 多路召回。
  21. 为什么 RAG 要建索引?为什么不能每次直接扫全部知识库?
  22. 你觉得除了提问式的对话,Agent 还有什么其他形态?
  23. 二叉树前序、中序、后序、层序遍历分别是什么?
  24. DFS 为什么常用栈或递归,BFS 为什么常用队列?
  25. 手撕:二叉树的层序遍历。
  26. 反问环节:你有什么想问面试官的?

《参考解析》

从 JSX 到浏览器渲染:先把「语法糖—编译—渲染」这条线串起来

JSX 本质是语法糖,浏览器完全不认,它在构建期被转换成函数调用(经典运行时是 React.createElement,新的自动运行时是 jsx/jsxs 并用 jsxImportSource 自动引入),产出的仍是普通 JavaScript,所以才需要构建工具。Babel 是源码到源码的编译器,工作分三个阶段:@babel/parser 把源码解析成 AST,插件在 transform 阶段遍历并改写 AST,@babel/generator 再把 AST 还原成代码并生成 sourcemap;插件按访问者模式匹配节点类型,这也是写自定义 Babel 插件的入口。和 polyfill 的区别要落在判据上:Babel 的 preset-env 做的是「语法降级」——箭头函数、可选链、类字段这类新语法能改写成旧语法;而 Promise、Array.prototype.includes、Object.fromEntries 这类是运行时 API,旧环境根本没有对应实现,改语法改不出来,必须靠 core-js 之类的垫片注入。判断标准就是「这是语法还是 API」。

浏览器的渲染流程从导航开始:URL 解析后走 DNS、TCP、TLS,收到 HTML 后边下载边解析构建 DOM(解析中遇到同步 script 会阻塞,defer 是解析完按序执行、async 是下载完就执行),同时构建 CSSOM,两者合成渲染树后进入样式计算、布局(计算几何位置)、绘制(生成绘制指令)、合成(分层后交给 GPU 合成)。性能优化的常见落点都挂在这条线上:改几何属性触发重排,改颜色等触发重绘,transform 与 opacity 可以只走合成层;频繁读取布局属性再写入会造成强制同步布局(布局抖动),要用读写分离或 requestAnimationFrame 批处理;长任务会阻塞主线程,所以配合 will-change、虚拟列表、代码分割等手段控制单帧工作量。

React 的更新机制:Hooks 规则、三种缓存与虚拟 DOM 的价值

Hooks 有几个容易考到却容易被忽略的点:Hooks 是按调用顺序存在链表上的,所以不能在条件判断或循环里调用,否则顺序错位、状态全乱;useEffect 依赖数组为空表示只在挂载后执行一次,返回的函数在卸载时执行,而开发环境的严格模式会故意执行两遍以暴露副作用没清理的问题;依赖数组写漏会导致闭包捕获旧值,这是实际项目里最隐蔽的一类 bug。

三种「缓存」要讲清各自缓存的对象:useMemo 缓存计算结果,避免每次渲染都重算昂贵逻辑;useCallback 缓存函数引用,好处体现在把函数传给用 React.memo 包裹的子组件或作为其它 Hook 的依赖时;React.memo 缓存组件本身的渲染结果,靠浅比较 props 决定是否跳过渲染。三者只有在「引用相等性真的被下游用来做判断」时才有效,否则只是多了一层比较和内存占用——判断方法很简单:看这个值有没有进依赖数组、下游组件有没有 memo。

Virtual DOM 的价值不是「比直接操作 DOM 快」,而是把 UI 描述成数据、让声明式写法与批量更新成为可能,再用 diff 求出最小更新集。React 的 diff 用的是几条启发式:只做同层比较(跨层移动会被当成删除加新建)、类型不同直接重建子树、列表靠 key 判断节点身份——用数组下标当 key 在列表顺序变化时会导致状态错位,这是常被追问的一个点。要主动补一句:手写精细的 DOM 操作在单点上可以比 diff 快,虚拟 DOM 换来的是一致性、可组合和维护成本。

JSBridge、WebView 与 React Native:跨端的三条路

WebView 是宿主 App 提供的浏览器内核容器,H5 在里面跑的是和浏览器一致的那套渲染流程,区别在于多了与 Native 的通信边界:能力受限(拿不到全部系统 API)、生命周期受宿主控制、还要处理与原生页面的滚动和返回手势协调。JSBridge 就是把这个边界打通的一层协议。JS 调 Native 的常见实现有几条:拦截 URL scheme(iframe.src 或 location.href,实现简单但参数长度与性能受限)、拦截 prompt/console(H5 里比较古老的做法)、注入对象(Android 的 addJavascriptInterface)、以及 iOS 上 WKWebView 的 WKScriptMessageHandler(现在是主流)。Native 回调 JS 则是通过 evaluateJavascript 执行一段调用回调的脚本,工程上要给每次调用分配 callbackId,在 JS 侧维护回调注册表,把异步的桥接调用包装成 Promise,并配超时与重试;注入时机也要注意——桥对象通常要等页面加载到特定阶段才可用,所以调用前要探测或等待 ready 事件。安全上必须校验来源域名、限制可调用的方法白名单,避免暴露任意系统能力。

React Native 走的是另一条路:不用 WebView,JS 线程通过 Bridge 与原生通信(新架构换成 JSI 直连加 Fabric 渲染与 TurboModules),布局由 Yoga 按 flex 规则算出,再映射成真实原生视图,所以渲染结果是原生控件、性能与体验比 WebView 方案好,代价是生态与一致性成本、以及跨线程通信仍有开销。和小程序、Flutter 这类方案对比时可以概括成三种思路:WebView 套壳(Web 一套代码,体验上限最低)、自绘引擎(Flutter 在 Skia 上画,一致性最好但要自带控件体系)、原生映射(RN 把组件映射成原生视图,介于两者之间)。

短轮询、SSE 与 WebSocket:AI 流式输出为什么选 SSE

三者的差别用「谁先说话、要不要双向、连接维持成本」三个问题就能分清。短轮询是客户端定时问,实现最简单、服务端无状态,但延迟等于轮询间隔、大量请求是空转,只适合状态更新不频繁且对延迟不敏感的场景;长轮询是服务端 hold 住请求直到有数据或超时,实时性比短轮询好,但每来一次数据就要重建连接,服务端也占着连接资源;SSE 是服务端在一条 HTTP 连接上单向推送,响应类型是 text/event-stream,浏览器自带重连与 Last-Event-ID 续传语义,纯文本、易调试、穿过代理无压力;WebSocket 是握手后升级成独立协议的全双工连接,双方随时可发,适合聊天、协同编辑、需要低延迟双向心跳的场景,代价是要自己做心跳、重连、鉴权和连接治理。

SSE 适合 AI 流式输出的原因很直接:模型的输出是单向、逐 token 增量产生的,正好匹配单向推送;文本帧省去二进制协议的编解码;在网关后面比 WebSocket 更容易透传。断线补发的做法是给每个事件带递增 id(例如消息分片序号)和事件类型,客户端重连时浏览器自动在请求头带上 Last-Event-ID,服务端据此从该位置继续推;服务端还要处理「生成已经结束但连接断了」的情况——客户端重连时先按消息 id 回查已落库的完整内容,避免只拿到后半段。工程坑要一起说:Nginx 等反向代理必须关闭响应缓冲(否则会攒一批再吐,失去流式效果)、要放宽读超时、HTTP/1.1 同域名连接数有限会导致多个会话排队(HTTP/2 多路复用可缓解)、以及前后端要约定结束事件与错误事件,让前端能区分「正常结束」和「中途失败」。

Webpack 的构建流程与 Loader、Plugin 的分工

Webpack 的核心是「一切皆模块」加依赖图。流程大致是:从 entry 出发,用 resolver 定位模块路径,按 module.rules 匹配 loader 链(从右到左、从下到上依次执行)把非 JS 资源(CSS、图片、TS)转换成可被打包的模块,用解析器把源码转成 AST 找出 import/require 形成依赖图,递归处理直到没有新依赖;随后进入优化阶段——tree shaking(依赖 ESM 的静态分析与 sideEffects 标记)、splitChunks 分包、scope hoisting 把模块合并进同一作用域——再由生成器产出 chunk 与 asset,把最终的 bundle 与运行时(模块注册表、__webpack_require__)写入输出目录。开发模式下的 HMR 是 devServer 通过 WebSocket 通知客户端,客户端拉取变更模块并替换,同时把更新冒泡到边界模块。

Loader 与 Plugin 的区别可以用一句话概括:Loader 面向「文件内容」,是一个把源码字符串(或 buffer)转成模块的函数,在模块构建阶段被调用,职责单一、可以链式组合,例如 babel-loader、css-loader、ts-loader;Plugin 面向「构建流程」,是一个带 apply(compiler) 的类,通过 tapable 的钩子挂到编译生命周期的各个节点上,能做的事远超模块转换——产出额外文件、改配置、优化 chunk、注入变量,例如 HtmlWebpackPlugin、MiniCssExtractPlugin、DefinePlugin。回答时可以主动补一句实践:Loader 解决不了「构建产物级别」的事,所以像「把 CSS 抽成独立文件」这种需求必须用 Plugin。

Agent 工程:Skills 与 MCP 与 A2A、Harness、上下文与 RAG

这三个缩写是不同层次的协议/机制,别并列比较。MCP 解决「能连什么」——模型与外部工具、数据源之间的标准化协议,规定服务端如何暴露工具与资源、客户端如何发现与调用,目的是可插拔,换客户端或换模型不用重写集成。Skill 解决「该怎么做」——把某类任务的方法、步骤、约束组织成可被加载的知识文件,本质是程序性记忆,按需渐进式加载以省上下文,与 MCP 是互补关系:工具能力来自 MCP,使用策略来自 Skill。A2A 解决「谁来干、怎么交接」——Agent 与 Agent 之间的协作协议,关注任务委派、状态同步与跨进程/跨团队的互操作,比前两者更靠上层。它们可以同时存在:一个 Agent 通过 A2A 把子任务交给另一个 Agent,对方用 MCP 调工具、按 Skill 描述的流程执行。

Harness 想让 AI 按指定工作流执行,关键是把流程从提示词里拿出来交给代码:用状态机或图编排定义节点与条件边,每个节点只注入该步需要的上下文和工具白名单,输入输出用 schema 约束并校验,失败按策略重试或转人工,全程留痕可回放。上下文管理的思路是分层装配——把「系统约束与当前状态」设为必留、「本轮任务相关内容」设为按需、「历史与检索结果」设为可裁剪;长历史做摘要或滚动窗口,工具返回只保留结论与引用而不是原始大段文本,检索结果先 rerank 去重再截断,能复用前缀的稳定内容放在前面以命中提示缓存。超出预算时优先丢弃旧对话与已完成步骤的中间过程,保留约束、最新状态和未完成的计划;即使用长上下文模型也要做这些,因为成本、延迟和注意力稀释三个问题不会因为窗口变大而消失。

RAG 多路召回是把不同检索通道的结果融合后统一精排:向量召回负责语义相近(同义改写、口语化问题),关键词或 BM25 负责精确实体与编号(型号、错误码、专有名词,这正是向量容易漏的),元数据过滤负责时间、部门、权限这类结构化约束,再加父文档或摘要召回解决「片段太碎、缺少上下文」的问题;多路结果按倒数排名融合或加权合并,交给 cross-encoder 精排后取 top-k 送入生成。为什么要建索引:全量扫描是 O(N) 且每次都要算相似度,延迟与成本随库增长不可接受,索引把检索变成亚线性的近似最近邻查找(HNSW、IVF 这类),同时支持过滤与增量更新;代价是召回率不再百分之百,所以要用关键词路和精排把漏召回兜回来。关于 Agent 的其他形态,可以从交互方式上答:固定工作流型(把确定流程编排成图,稳定可审计)、事件驱动或后台常驻型(监听消息、定时巡检、异常主动介入)、多智能体协作型(分工加评审,用互相检查换质量)、以及操作型(浏览器与桌面自动化,代替人点界面),再补一句形态的选择取决于任务的可预测性与错误的代价。

手撕层序遍历与 DFS/BFS 的取舍

遍历本身要能分清「根的位置」:前序是根左右、中序是左根右、后序是左右根,递归写法三行;层序不用递归,用队列做 BFS。手撕层序遍历的模板是:根入队,每轮先记录当前队列长度 size,循环 size 次出队并把左右子节点入队,这一轮出队的节点就是同一层,收集成数组。要主动提几个变体:之字形遍历(按层号反转偶数层)、每层取最大值、从下往上输出(最后反转结果);用 DFS 也能做层序,代价是要带深度参数并按深度往结果里填,空间从「最大宽度」变成「树高」,这正好引出 DFS 与 BFS 的取舍。

为什么 DFS 用栈或递归、BFS 用队列,要答到数据结构与访问顺序的对应关系上:DFS 需要「后进先出」才能回到最近的岔路继续深入,递归本质上就是函数调用栈;BFS 需要「先进先出」才能保证同层的节点先于下一层被处理,所以用队列。空间复杂度上,DFS 与树高(或图的深度)相关,BFS 与最大宽度相关,所以宽而浅的树用 DFS 省内存、深而窄的树用 BFS 更稳(DFS 还可能爆栈,可以改写成显式栈或加深度上限);图上的遍历两者都要 visited 去重,BFS 的另一个用途是在无权图上求最短路径,因为它是按层扩展的。