面灵AI→

元戎启行秋招前端一面:Hooks与Promise串行

轮次
一面
结果
已挂
时间
2026-09
来源
牛客网

《面试题目》

  1. 讲一下对闭包的理解。
  2. 浏览器事件循环是怎样的?
  3. 浏览器输入 URL 到页面完整渲染的全过程是什么?HTTP 常见状态码有哪些?
  4. React Hooks:为什么不能放在条件语句内?它的底层数据结构是什么?
  5. 笔试:事件循环输出题(宏任务、微任务执行顺序)。
  6. 笔试:实现一个方法,接收数组与回调函数,遍历数组执行回调,保证异步 Promise 有序输出,禁止使用 async/await。
  7. 反问:岗位业务是什么?(答:Web 端为主,少量移动 H5,多为公司内部平台,如 AI 平台、数据/模型管理平台)
  8. 反问:技术栈是什么?(答:React、Vue 均有使用)

《参考解析》

闭包:闭包是「函数 + 它定义时所处的词法环境」的组合,即函数记住了自己定义位置能访问的变量,即使外层函数已经返回,这些变量仍然存活在堆上。它的价值在于封装私有状态(模块模式、计数器、防抖节流里的定时器引用)、实现函数工厂(柯里化)、以及在回调中保留上下文。代价要说清楚:被闭包引用的变量无法被 GC 回收,长生命周期闭包持有大对象就会造成内存泄漏,典型事故是事件监听/DOM 引用没解绑、定时器没清除、缓存 Map 无上限增长。经典考题是循环里 var 与 let 的区别(var 共享同一个变量,let 每次迭代创建新的绑定,所以 let 版本打印 0/1/2,var 版本打印 3/3/3),以及如何用 IIFE 或 let 修正。另外要提防「闭包捕获的是变量的引用而不是值」,循环里异步操作用错就是踩这个。

浏览器事件循环:JS 是单线程的,靠事件循环在调用栈、任务队列之间调度。一次 tick 的顺序是:执行同步代码 → 清空微任务队列(清空过程中新增的微任务也在本轮继续执行)→ 若需要则进行渲染(requestAnimationFrame 回调 → 样式计算/布局/绘制)→ 取下一个宏任务执行 → 再清空微任务,如此循环。微任务包括 Promise.then/catch/finally、queueMicrotask、MutationObserver;宏任务包括 setTimeout/setInterval、I/O、UI 事件回调、MessageChannel(注意 setTimeout 有最小 4ms 延迟与嵌套层级惩罚)。与 Node 的区别:Node 的事件循环分 timers、pending callbacks、poll、check、close 等阶段,setImmediate 在 check 阶段执行,process.nextTick 拥有比 Promise 更高的优先级(在每个阶段之间清空),因此主模块中 setTimeout(fn,0) 与 setImmediate 的顺序不确定,但在 I/O 回调内部 setImmediate 一定先执行。做输出题的方法就是分层填表:同步段 → 微任务 → 宏任务逐个展开。

URL 到渲染的全过程:① URL 解析与补全(协议、主机、端口、路径、查询);② 查缓存:先看浏览器缓存(强缓存直接命中就结束)、Service Worker、内存缓存,再看 DNS 缓存;③ DNS 解析(浏览器缓存 → 系统 hosts → 本地 DNS → 递归到根/顶级/权威服务器,得到 A/AAAA 记录);④ 建立连接:TCP 三次握手(HTTPS 还要 TLS 握手,1.3 是一轮 RTT,含证书校验与密钥协商)、可能命中 HTTP/2 或 HTTP/3 复用连接;⑤ 发请求:拼 HTTP 报文(方法、路径、头部如 Cookie/Accept/UA),服务端处理(负载均衡 → 应用 → 数据库/缓存);⑥ 收响应:状态码、响应头(Content-Type、Content-Encoding、缓存头)、响应体;⑦ 解析:HTML 字节流经解码、分词、建 DOM 树,同时 CSS 建 CSSOM,遇到脚本默认阻塞解析(async/defer 可优化),遇到图片等非阻塞资源并行下载;⑧ 构建渲染树(DOM + CSSOM,跳过 display:none 节点),布局(layout/reflow)计算几何位置,绘制(paint)生成绘制指令,合成(composite)分层后由 GPU 输出;⑨ 后续的 JS 执行、事件绑定、懒加载与字体替换(FOUT/FOIT)会触发重排重绘。常见状态码:200 成功、204 无内容、206 部分内容(断点续传)、301 永久重定向(会被缓存)、302/307 临时重定向、304 协商缓存命中、400 参数错误、401 未认证、403 无权限、404 不存在、405 方法不允许、413 体积过大、429 限流、500 服务端异常、502 网关收到非法响应、503 不可用、504 网关超时。

Hooks 为什么不能在条件语句里、底层是什么:React 内部用「按调用顺序存储的链表(fiber 上的 memoizedState 单向链表)」来保存每个 Hook 的状态,第 n 次调用的 Hook 对应链表第 n 个节点。如果某个 Hook 被包在 if 里,某次渲染跳过它,后续 Hook 的读取位置就会整体错位——useState 拿到的是别的 Hook 的状态,useEffect 的依赖比对也会错乱。这也是「Hooks 必须在组件顶层、循环/嵌套函数里也不行」的根本原因,以及为什么需要 eslint-plugin-react-hooks 的 rules-of-hooks 规则。规则可以通过 key 强制重置组件、或把条件逻辑挪到 Hook 内部(useState(cond ? a : b)、在 useEffect 里判断)来合规。相关底层细节值得一并说:useState 更新靠 dispatchAction 挂在 Hook 队列上(queue.pending 环形链表),React 18 引入自动批处理与并发特性后,更新按优先级调度、可中断可重放,所以渲染阶段必须是「无副作用、纯粹按顺序」的。

Promise 串行执行(禁用 async/await):核心是「用 then 把数组折叠成一条链」,或者用递归推进下标。两种写法:

// 写法一:reduce 折叠成链
function runSerial(list, iterator) {
  return list.reduce(
    (chain, item, index) =>
      chain.then(() => Promise.resolve(iterator(item, index))),
    Promise.resolve()
  );
}

// 写法二:递归推进(可提前中断)
function runSerialRecursive(list, iterator, index = 0) {
  if (index >= list.length) return Promise.resolve();
  return Promise.resolve(iterator(list[index], index)).then(() =>
    runSerialRecursive(list, iterator, index + 1)
  );
}

要点有四个:一是必须把每次回调的返回值(可能是 Promise)用 Promise.resolve(...) 包一层再 then,否则同步返回值和 Promise 混在一起会破坏串行;二是 reduce 里不能直接写 chain.then(iterator),因为 then 会把 iterator 的返回值当成新的链值,而我们要的是「等它完成」;三是要支持串行收集结果(把返回值 push 进数组,最后 resolve 数组)和出错即停(任一 rejected 就整体 rejected,除非要求「出错也继续」——那就在 step 里 catch 掉);四是禁止 async/await 时不要偷偷用 await,也不能用 Promise.all(它并发)。如果还想加「上一个结果传给下一个」,把 then 的值顺着链传下去即可。另外可以提一句:这种串行模式在「必须按序调用接口/写文件」的场景常用,但性能上比并发慢,若能并发就该用 Promise.all + 并发上限控制。

面完复盘的常见教训:这场面试「聊得挺好但挂了」的情况在秋招里很常见,通常不是表达问题,而是某个技术点没答到面试官预期的深度,或者岗位 HC 与候选人画像不匹配。可操作的复盘方式是当天回忆并记下所有题目与自己的答法,对照参考答案找出「只答了结论、没答原理/边界/取舍」的地方(比如闭包有没有说到内存泄漏与循环陷阱、Hooks 有没有讲到链表错位),下次面试前重刷一遍。另外反问环节问到的信息(业务是内部平台、技术栈 React/Vue 混用)也可以用来判断自己是否真的想去。