一、微前端(qiankun 相关)
- **你实习项目里面用到了 qiankun,应该是这个微前端架构,你对微前端架构有了解吗?**微前端是一种将前端应用拆分成多个可独立开发、测试、部署的子应用,再通过主应用进行整合的架构模式。
- 核心目标:解决巨石应用的维护、协作和性能问题,实现团队解耦、技术栈无关、增量升级。
- 典型场景:大型 SaaS 平台、多团队协作的复杂系统、需要兼容旧技术栈的项目。
- 代表方案:qiankun、single-spa、无界、EMP 等。
- **来讲一下微前端它的一些实现方案?**主流实现方案分为四类:| 方案 | 原理 | 优缺点 | 代表 || :--- | :--- | :--- | :--- || 路由分发式 | Nginx / 网关根据路由转发到不同应用 | 实现简单,无前端耦合;但页面刷新会跳转,体验差 | 早期多站点架构 || iframe 方案 | 主应用通过 iframe 嵌入子应用 | 天然隔离 JS/CSS,实现简单;但通信复杂、体验差、性能差 | - || JS 沙箱式(qiankun 等) | 主应用加载子应用 JS/CSS,通过沙箱隔离环境 | 体验接近单页应用,技术栈无关;但沙箱有兼容性和性能成本 | qiankun、single-spa || Web Components 方案 | 利用 Shadow DOM 和自定义元素封装子应用 | 原生隔离,技术无关;但生态不完善,兼容性需处理 | - |
- **主应用和子应用沙箱隔离是怎么实现的?**qiankun 的沙箱分为两种:
-
JS 沙箱:
- 旧版
LegacySandbox:通过代理window对象,记录子应用对全局变量的修改,切换时还原。 - 新版
ProxySandbox:基于 ES6Proxy,为每个子应用创建独立的window代理对象,子应用的读写都在代理上完成,不会污染真实window。
- 旧版
-
CSS 隔离:通过样式隔离方案避免样式污染(见第 6 题)。
- **A 应用的会影响到 B 子应用的,所以你在他们两个子应用切换的时候,需要把这种我们的 window 给还原一下。**这是 qiankun 沙箱的关键逻辑:
- 子应用运行时对
window的属性修改,会被沙箱记录下来。 - 子应用卸载时,沙箱会执行快照还原,将
window对象恢复到子应用加载前的状态,避免对后续子应用造成影响。 - 比如子应用 A 给
window挂载了全局变量,切换到子应用 B 时,A 的修改会被清除,B 的环境是干净的。
- 微前端像一个这种代理沙箱,它像能单实例还是多实例?Window 这一块是怎么实现的呢?
-
qiankun 的
ProxySandbox是多实例的:每个子应用都有独立的window代理实例,互不干扰。 -
实现原理:
- 创建一个
fakeWindow对象,继承自window。 - 通过
new Proxy(fakeWindow, handler)创建代理,拦截子应用对window的读写操作。 - 子应用读取
window属性时,优先从代理对象中读取,再回退到真实window。 - 子应用修改
window属性时,修改的是代理对象,不会影响真实window。
- 创建一个
- **css 怎么隔离?**qiankun 提供了三种 CSS 隔离方案:
scoped样式隔离(Shadow DOM) :把子应用挂载到Shadow DOM中,利用浏览器原生的样式隔离能力,样式不会泄漏到外部。缺点是兼容性稍差,且子应用的全局样式(如body样式)会失效。strictStyleIsolation(实验性) :通过动态修改子应用的 CSS 选择器,添加前缀(如div→div[data-qiankun-appid]),限制样式作用域。缺点是会修改原始 CSS,存在兼容性风险。- 约定式隔离:业务层通过 CSS Modules、CSS-in-JS、BEM 命名规范(如
app1-button)来避免样式冲突,这是最稳妥的方案。
二、WebWorker 相关
- **看你实习项目里用到了 webWorker,这个是用来干什么呢?**WebWorker 是浏览器提供的多线程解决方案,它允许 JS 代码在主线程之外的后台线程中运行。
-
核心用途:
- 执行耗时的计算任务(如大数据量的解析、计算、排序、复杂正则匹配),避免阻塞主线程,导致页面卡顿。
- 处理离线数据同步、大文件分片上传 / 解析、canvas 离屏渲染等场景。
- **它为什么能阻塞主线程?**这个问题是反的,WebWorker 不会阻塞主线程,它的核心价值就是避免阻塞主线程:
- JS 是单线程的,主线程执行耗时任务时,会阻塞事件循环,导致页面渲染、交互无响应。
- WebWorker 运行在独立的线程中,和主线程通过
postMessage通信,不会占用主线程的执行时间,因此不会阻塞主线程。 - 补充:如果主线程频繁和 WebWorker 通信,大量数据传递(未使用 Transferable Objects)也会造成主线程的性能开销,但不是阻塞。
- **网络请求不做 Web worker 处理,请求过程中它会放在主线程吗?**不会阻塞主线程。
- 浏览器的网络请求(
fetch/XMLHttpRequest)是由浏览器的网络线程池处理的,属于异步 I/O 操作,不会占用主线程的执行时间。 - 请求的回调(如
onload/.then())会在主线程的事件循环中被处理,但请求的发送、响应接收都在浏览器后台线程完成,不会阻塞主线程。
- **异步回调会影响吗?**异步回调本身不会阻塞主线程,但回调中的代码如果是同步的耗时任务,依然会阻塞主线程。
- 比如
fetch请求的.then()回调里执行了一个百万级数组的循环,这个同步代码会在主线程执行,造成页面卡顿。 - WebWorker 就是用来把这类耗时的同步计算放到后台线程执行的。
三、请求优化与 Promise 相关
- 那你现在这种你们这种接口请求的这种数据量大吗?(按你项目实际回答,这里给一个通用模板)
- 我们的业务场景下,部分接口会返回较大的数据集(比如批量数据导出、列表数据同步),单接口数据量可达几 MB 甚至几十 MB,直接在主线程处理会造成页面卡顿,因此我们做了请求池、流式处理和 WebWorker 解析等优化。
- **Promise.allSet 这一块里面是一些什么样的逻辑呢?**你说的应该是
Promise.allSettled,它的核心逻辑:
- 接收一个 Promise 数组作为输入。
- 等待所有 Promise(无论成功还是失败)都执行完成后,返回一个新的 Promise。
- 新 Promise 的结果是一个数组,每个元素对应输入的 Promise 的结果,包含
status(fulfilled/rejected)和value/reason。 - 核心特点:不会因为某个 Promise 失败而终止,会收集所有 Promise 的最终状态。
- Promise.allSet 它是 5 个一批,它是上面请求完了再进行下面的请求还是有一个请求池一直在维护这个请求?
Promise.allSettled本身不控制并发数量,它会同时发起所有请求。你说的 “5 个一批” 是手动实现的请求池 / 并发控制逻辑,不是Promise.allSettled自带的:
- 实现方式:维护一个最大并发数(比如 5),先发起前 5 个请求,每当有一个请求完成(无论成功失败),就从剩余请求列表中取出下一个补充进来,始终保持并发数不超过上限。
- 它不是 “一批请求全部完成再发起下一批”,而是动态维护请求池,保证请求数不超过上限,最大化利用并发能力。
- **请求池的概念?**请求池(并发请求控制)是为了避免同时发起过多请求导致浏览器并发限制或服务端压力过大的一种优化手段:
-
核心逻辑:维护一个固定大小的 “请求池”,控制同时进行的请求数量。
-
实现原理:
- 维护一个待执行的请求队列。
- 同时发起 N 个请求(N 为最大并发数)。
- 每当有请求完成,就从队列中取出下一个请求执行,直到队列为空。
-
目的:规避浏览器同域名并发请求限制(HTTP/1.1 下 Chrome 默认 6 个),同时减轻服务端压力。
- **这里是 25 个是直接同一批次的给它 push 到这种候选池里面吗?**是的,25 个请求会先全部加入候选队列,然后请求池会根据设置的最大并发数(比如 5),每次取出 5 个并发执行,执行完成一个就补充一个,直到 25 个全部完成。
四、HTTP 相关
- 你们用的 http 请求是 http1 还是 http2(按你项目实际回答,通用模板)
- 我们的服务端已经支持 HTTP/2,前端在支持的环境下优先使用 HTTP/2,降级使用 HTTP/1.1。HTTP/2 的多路复用特性,对我们的批量请求场景有明显的性能提升。
- 多路复用和长链接是什么?
-
长连接(Persistent Connection) :
- HTTP/1.1 默认开启
Connection: keep-alive,客户端和服务端建立 TCP 连接后,不会立即关闭,后续请求可以复用同一个连接,减少 TCP 握手的开销。 - HTTP/1.1 的长连接有缺陷:同一个连接同一时间只能处理一个请求(队头阻塞)。
- HTTP/1.1 默认开启
-
多路复用(Multiplexing) :
- HTTP/2 在同一个 TCP 连接上,允许同时发送多个请求和响应,请求和响应可以乱序发送,通过流 ID 区分,彻底解决了 HTTP/1.1 的队头阻塞问题。
- http2 除了多路复用还有哪些新特性呢?
- 二进制分帧:将 HTTP 报文拆分为二进制帧传输,比文本协议更高效、紧凑。
- 头部压缩(HPACK) :对 HTTP 头部进行压缩,减少重复头部的传输开销。
- 服务器推送(Server Push) :服务端可以主动向客户端推送资源,提前加载客户端未来需要的静态资源,减少请求次数。
- 请求优先级:可以为不同请求设置优先级,浏览器优先处理高优先级的请求。
- **http3 这一块有了解吗?**HTTP/3 是下一代 HTTP 协议,核心变化是底层传输协议从 TCP 换成了 QUIC(基于 UDP):
-
核心优势:
- 彻底解决队头阻塞:QUIC 的每个流是独立的,一个流的丢包不会影响其他流。
- 连接建立更快:QUIC 的握手可以和 TLS 握手合并,0-RTT/1-RTT 连接建立,减少延迟。
- 连接迁移:QUIC 连接基于连接 ID,IP / 端口变化时连接不会断开,适合移动网络。
-
目前主流浏览器和服务端(Nginx、CDN)都已支持,逐步普及中。
-
**http2 它一次连接可以并行多个请求,建立连接步骤是什么样子?**HTTP/2 基于 TCP+TLS(h2)或明文 TCP(h2c),建立步骤:
-
TCP 三次握手:客户端和服务端建立 TCP 连接。
-
协议协商:
- TLS 场景:客户端在
ALPN扩展中发送h2,服务端确认支持 HTTP/2。 - 明文场景:客户端发送
Upgrade: h2c请求,服务端响应101 Switching Protocols。
- TLS 场景:客户端在
-
SETTINGS 帧交互:客户端和服务端交换
SETTINGS帧,协商参数(如最大并发流数、初始窗口大小)。 -
连接建立完成:之后就可以在同一个 TCP 连接上并行发送多个请求流了。
-
**三次握手第二次丢失了会怎么样?**三次握手过程:
-
客户端发送 SYN 包(第一次)。
-
服务端收到 SYN,回复 SYN+ACK 包(第二次)。
-
客户端收到 SYN+ACK,回复 ACK 包(第三次)。
如果第二次的 SYN+ACK 包丢失:
- 客户端迟迟收不到服务端的 SYN+ACK,会触发 SYN 超时重传,重新发送 SYN 包。
- 服务端如果一直没收到客户端的第三次 ACK,会将连接放入半连接队列,等待客户端的 ACK 超时,超时后会发送 RST 包关闭半连接。
- 哪一边的超时重试?会进行什么处理?
- 客户端的超时重试:客户端的 SYN 包发送后,会设置超时计时器,超时未收到 SYN+ACK,就会重发 SYN 包,重传次数由操作系统配置决定(通常 2-3 次)。
- 服务端的半连接超时:服务端收到 SYN 后,回复 SYN+ACK,进入半连接状态,等待客户端的 ACK。如果超时未收到 ACK,会将半连接从队列中移除,并发送 RST 包。
- **https 在 http 上面做了哪些处理?**HTTPS = HTTP + TLS/SSL,在 HTTP 和 TCP 之间增加了 TLS 加密层:
- 传输的数据会被加密,防止明文传输被窃听、篡改。
- 通过数字证书验证服务端身份,防止中间人攻击。
- 握手过程会协商加密套件和会话密钥,保证传输安全。
-
**简单说下加密连接过程,密钥怎么来的?用到哪些加密方案?**HTTPS 连接建立过程:
-
客户端发起连接:客户端发送
Client Hello,携带 TLS 版本号、支持的加密套件、随机数client random。 -
服务端响应:服务端回复
Server Hello,确认 TLS 版本、加密套件、随机数server random,并发送服务端的数字证书。 -
证书验证:客户端验证证书的有效性(CA 签名、域名匹配、有效期),确认服务端身份。
-
会话密钥协商:
- 客户端生成预主密钥
pre-master secret,用服务端证书的公钥加密后发送给服务端。 - 服务端用私钥解密得到
pre-master secret。 - 双方通过
pre-master secret、client random、server random生成会话密钥。
- 客户端生成预主密钥
-
加密通信:双方使用会话密钥,通过对称加密算法(如 AES)加密后续的 HTTP 数据传输。
用到的加密方案:
- 非对称加密:RSA/ECC,用于加密预主密钥,验证服务端身份。
- 对称加密:AES/ChaCha20,用于实际数据传输加密,效率更高。
- 哈希算法:SHA-256,用于消息认证(HMAC),防止数据篡改。
- 分对称加密和对称加密分别用在什么阶段?
-
非对称加密:
- 证书验证阶段:验证服务端证书的签名,确认服务端身份。
- 会话密钥协商阶段:客户端用服务端公钥加密预主密钥,服务端用私钥解密,保证预主密钥不被窃听。
-
对称加密:
- 数据传输阶段:会话密钥协商完成后,双方用会话密钥(对称密钥)加密 HTTP 数据,效率高,适合大量数据传输。
五、性能优化与框架相关
- **你简历这里提到了虚拟列表的这种优化方案,怎么实现的?**虚拟列表(Virtual List)的核心原理是:只渲染可视区域内的列表项,通过滚动位置动态计算需要渲染的项,减少 DOM 节点数量,提升性能。实现步骤:
- 计算可视区域:根据容器高度和滚动位置,计算当前可视区域的起始索引和结束索引。
- 占位容器:通过
padding-top/padding-bottom撑开列表容器的高度,模拟完整列表的滚动效果。 - 渲染可视项:只渲染起始索引到结束索引之间的列表项,设置绝对定位,根据索引和项高度计算每个项的
top位置。 - 滚动监听:监听容器的
scroll事件,动态更新可视区域的起始 / 结束索引,重新渲染。 - 它滚动怎么保证展示的区域是有数据,其他是没有,这一块怎么做?核心是视口计算 + 绝对定位 + 占位容器:
- 所有不在可视区域内的项,不会被渲染成 DOM 节点,只有可视区域内的项会被渲染。
- 通过
padding-top/padding-bottom模拟完整列表的高度,让滚动条的行为和真实列表一致。 - 每个渲染的项通过
position: absolute+top属性定位,确保滚动时位置正确。
-
你在上海赛淘数据有限公司用到 Astro, 简单说下它和我们日常用到的 SSR 的一些区别?Astro 是一个静态站点生成器(SSG),和传统 SSR(如 Next.js SSR/Remix)的核心区别:| 特性 | Astro | 传统 SSR || :--- | :--- | :--- || 渲染方式 | 默认静态生成(SSG) ,构建时生成 HTML,支持增量静态再生(ISR)、服务端渲染(SSR) | 运行时在服务端渲染 HTML(每次请求生成) || 客户端 JS | 默认零 JS,仅引入需要的交互组件(Island 架构), hydration 粒度更细 | 通常会发送完整的 React/Vue 运行时, hydration 开销大 || 框架无关 | 支持 React/Vue/Svelte 等多种框架,组件可以混合使用 | 通常绑定特定框架(如 Next.js 绑定 React) || 适用场景 | 内容型网站、文档站、博客、企业官网,对首屏性能要求高 | 动态内容多、需要服务端数据实时更新的应用 |
-
**Astro 怎么做一些组件通信呢?每个组件都单独监听吗?**Astro 组件通信分两种场景:
-
服务端组件(.astro) :
- 没有运行时,无法直接通信,通过
props父传子,或者通过Astro.props、上下文传递数据。 - 可以通过顶层的
layout.astro向所有子组件传递全局数据。
- 没有运行时,无法直接通信,通过
-
客户端组件(框架组件,如 React/Vue) :
- 和原框架的通信方式一致:React 组件用
props、Context API、状态库(Redux/Zustand);Vue 组件用props、事件、Pinia。 - Astro 的 Island 架构下,不同框架的客户端组件无法直接通信,需要通过全局状态(如
window上的状态对象)或自定义事件(CustomEvent)实现跨框架通信。
- 和原框架的通信方式一致:React 组件用
-
复杂数据 url 不方便传,有什么其他方案?
-
前端侧:
- 用
localStorage/sessionStorage存储数据,通过路由 hash 或简单 ID 传递。 - 用
POST请求传递数据,而非 URL 参数。 - 用状态管理库(如 Pinia/Redux)存储跨页面 / 跨组件数据。
- 用
-
服务端侧:
- 将复杂数据存到服务端缓存(如 Redis),传递缓存 Key。
- 用短链服务,将长 URL / 复杂数据映射为短 ID 传递。
- **Astro 这个架构是跨框架嘛,你一个 react 的状态库,用 vue 是不好取的,有什么其他方案吗?**Astro 支持多框架组件,但不同框架的状态库(如 React 的 Zustand 和 Vue 的 Pinia)无法直接互通,解决方案:
- 全局状态(原生 JS) :创建一个纯 JS 的状态对象,挂载到
window上,React 和 Vue 组件都可以读写。 - 自定义事件:通过
CustomEvent进行跨框架通信,一方触发事件,另一方监听。 - 轻量级跨框架状态库:使用无框架依赖的状态库,如
nanostores、zustand(也支持非 React)、recoil等。 - 服务端状态:将共享数据放到服务端,前端通过 API 拉取,避免客户端跨框架状态共享的问题。
- Astro 它能够共享它父节点一些这种 context,一些这种 props 吗?
-
服务端组件(.astro):
- 可以通过
props实现父传子,父组件向子组件传递数据。 - 可以通过
Astro.context或Astro.props共享数据,layout 组件可以向所有子页面传递全局数据。
- 可以通过
-
客户端组件:
- 不同框架的组件之间无法共享对方的 Context API,只能通过 Astro 的 props 传递简单数据,或用上面说的跨框架通信方案。
六、React 相关
- React 的 hooks 是怎么实现的?React 底层怎么来识别这些 hook?React Hooks 的实现基于链表结构和顺序依赖:
-
底层原理:
- 每个组件 fiber 节点上维护一个
memoizedState链表,存储该组件所有 hooks 的状态。 - 组件渲染时,hooks 按调用顺序依次执行,React 通过调用顺序来匹配链表中的状态节点。
- 每次渲染时,hooks 的调用顺序必须和上一次一致,否则会导致状态匹配错误(这就是为什么 hooks 不能在条件 / 循环中调用)。
- 每个组件 fiber 节点上维护一个
-
识别 hook 的方式:React 内部通过
ReactCurrentDispatcher来区分不同渲染阶段的 hooks 实现,不同阶段(mount/update)使用不同的 dispatcher,从而实现不同的逻辑。
- useEffect 执行机制是什么时候?
useEffect的执行时机:
- 首次渲染:组件 DOM 渲染完成后,异步执行(在浏览器绘制之后)。
- 更新渲染:组件 DOM 更新完成后,异步执行(在浏览器绘制之后)。
- 清理函数:在下一次 effect 执行之前,或者组件卸载时执行。
- 注意:
useEffect总是在浏览器完成页面绘制之后执行,不会阻塞渲染。
-
**React 渲染的话会先更新一次有哪些阶段?React Fiber 有了解吗?**React 渲染分为两个阶段:
-
Render 阶段(可中断) :
- 从根节点开始,遍历 fiber 树,计算组件的更新(diff),生成更新后的 fiber 节点树。
- 这个阶段是可中断的,React 可以根据浏览器的空闲时间,分批处理任务,优先级低的任务可以被高优先级任务打断。
-
Commit 阶段(不可中断) :
- 将 Render 阶段生成的 fiber 树一次性提交到 DOM,进行真实的 DOM 更新。
- 分为三个子阶段:
before mutation(DOM 更新前)、mutation(DOM 更新)、layout(DOM 更新后)。 - 这个阶段是同步执行的,不可中断,避免用户看到不完整的 UI。
React Fiber 是 React 16 引入的新协调引擎,核心是将同步的递归渲染拆分成可中断的单元任务。
- 了解 React 的这个 Fiber 的一些工作原理吗?它为什么会有 Fiber 这个东西?它什么时候可以进行一些中断呢?做的什么改造让它可以随时中断?
-
为什么需要 Fiber:
- 旧的 React 渲染是同步递归的,一旦开始就无法中断,大组件渲染会阻塞主线程,导致页面卡顿、交互无响应。
- Fiber 的目标是实现可中断、可调度、可优先级控制的渲染,提升用户体验。
-
工作原理:
- 把组件树拆分成一个个独立的
Fiber节点(任务单元),每个 Fiber 节点包含组件的类型、状态、子节点、兄弟节点、父节点等信息。 - 用链表结构(child、sibling、return)遍历 fiber 树,遍历过程可以被中断和恢复。
- 把组件树拆分成一个个独立的
-
如何实现中断:
- 采用 ** 时间切片(Time Slicing)** 机制,利用浏览器的
requestIdleCallback(或 polyfill),在浏览器空闲时执行渲染任务。 - 每次执行一个 Fiber 节点后,检查是否还有剩余时间,如果没有则暂停渲染,把主线程还给浏览器处理交互、渲染等高优先级任务,下次空闲时再继续执行。
- 采用 ** 时间切片(Time Slicing)** 机制,利用浏览器的
-
可中断的阶段:只有 Render 阶段(diff 计算)是可中断的,Commit 阶段(DOM 更新)是不可中断的,避免 UI 不一致。
七、跨端开发(Taro / React Native)
- 看你也接触到一些跨端开发,Taro 和 React Native 简单讲一下这两个方案如何实现跨端?跨端的底层了解吗?最后怎么渲染这种?| 方案 | 跨端原理 | 渲染方式 || :--- | :--- | :--- || Taro | 基于编译时的跨端框架,用类 React/Vue 语法编写代码,编译时将代码转换为不同平台的语法(微信小程序、H5、React Native) | - 小程序:编译为 WXML/WXSS/JS,运行在小程序的 WebView 中。- H5:编译为标准 HTML/CSS/JS,运行在浏览器中。- React Native:编译为 React Native 的 JSX,通过 RN 的桥接层渲染原生组件。 || React Native | 基于运行时的跨端框架,用 JS 编写逻辑,通过桥接层调用原生组件和 API | - JS 层通过
Shadow Tree描述 UI,传递给原生层。- 原生层将 Shadow Tree 映射为原生组件(UIView/Android View),由原生渲染引擎渲染。 | - 渲染的话是我们 JS 层去做还是 Native 层去做?
-
Taro:
- 小程序 / H5:渲染由 WebView(浏览器)完成,JS 层只负责逻辑和 DOM 操作。
- React Native:JS 层负责逻辑和 UI 描述,最终渲染由 Native 层完成。
-
React Native:
- 渲染由 Native 层完成,JS 层只负责生成 UI 的结构信息,不直接操作视图。
- React Native 和 App 里写个 WebView 有什么区别?为什么要 React Native| 特性 | React Native | WebView || :--- | :--- | :--- || 渲染性能 | 渲染原生组件,性能接近原生 App,流畅度高 | 渲染 Web 页面,受限于 WebView 的性能,复杂页面容易卡顿 || 体验 | 支持原生手势、动画,体验接近原生 | 受限于浏览器的交互能力,部分原生交互体验差 || API 能力 | 可以直接调用原生 API,如相机、定位、推送等 | 需要通过 JSBridge 和原生通信,API 调用受限,性能差 || 包体积 | 有基础的 RN runtime,包体积比 WebView 方案大,但比纯原生小 | 包体积小,但 WebView 本身有系统开销 |
- 为什么用 React Native:为了兼顾跨端开发效率和原生级的性能体验,避免 WebView 的性能瓶颈和交互限制。
- **Bridge 这种桥接法是怎么沟通我们前端 webview?**JSBridge 是 WebView 和原生之间的通信桥梁,实现方式:
- 注入 JS 对象:原生向 WebView 注入一个全局的 JS 对象(如
window.webkit.messageHandlers或window.Android),JS 可以调用这个对象的方法,原生监听调用事件。 - URL Scheme 拦截:JS 通过
iframe.src或location.href发起特定协议的 URL 请求(如jsbridge://action),原生拦截 URL 请求,解析参数并处理。 - 消息队列:JS 层将消息放入队列,原生层通过轮询或回调的方式读取队列,处理后再将结果回调给 JS 层。
八、手写题
- 手写 ts 的 pick
typescript
运行
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// 使用示例
interface User {
name: string;
age: number;
address: string;
}
type UserNameAndAge = MyPick<User, 'name' | 'age'>;
// 结果:{ name: string; age: number }
41. 手写发布者订阅,加个 once
javascript
运行
class EventEmitter {
constructor() {
this.events = new Map();
this.onceEvents = new Map();
}
// 普通订阅
on(event, callback) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event).push(callback);
}
// 一次性订阅
once(event, callback) {
if (!this.onceEvents.has(event)) {
this.onceEvents.set(event, []);
}
this.onceEvents.get(event).push(callback);
}
// 发布事件
emit(event, ...args) {
// 执行普通订阅
if (this.events.has(event)) {
this.events.get(event).forEach(cb => cb(...args));
}
// 执行一次性订阅,执行后移除
if (this.onceEvents.has(event)) {
const callbacks = this.onceEvents.get(event);
this.onceEvents.delete(event);
callbacks.forEach(cb => cb(...args));
}
}
// 取消订阅
off(event, callback) {
if (this.events.has(event)) {
const callbacks = this.events.get(event).filter(cb => cb !== callback);
this.events.set(event, callbacks);
}
if (this.onceEvents.has(event)) {
const callbacks = this.onceEvents.get(event).filter(cb => cb !== callback);
this.onceEvents.set(event, callbacks);
}
}
}
// 使用示例
const emitter = new EventEmitter();
const log = (msg) => console.log(msg);
emitter.on('test', log);
emitter.once('testOnce', log);
emitter.emit('test', 'hello'); // 触发log
emitter.emit('testOnce', 'world'); // 触发log,之后once事件被移除
emitter.emit('testOnce', 'world2'); // 无反应
42. 事件循环题目(给一个典型题目 + 解析,方便你理解)
javascript
运行
console.log('start');
setTimeout(() => {
console.log('setTimeout1');
Promise.resolve().then(() => {
console.log('microTask1');
});
}, 0);
Promise.resolve().then(() => {
console.log('microTask2');
setTimeout(() => {
console.log('setTimeout2');
}, 0);
});
console.log('end');
执行顺序:
- 同步代码:
start→end - 微任务队列:
microTask2 - 宏任务队列:
setTimeout1 - 执行微任务:
microTask2,此时将setTimeout2加入宏任务队列 - 执行宏任务
setTimeout1,打印setTimeout1,并将microTask1加入微任务队列 - 执行微任务
microTask1 - 执行宏任务
setTimeout2最终输出顺序:start→end→microTask2→setTimeout1→microTask1→setTimeout2
核心规则:
- 先执行所有同步代码,再清空微任务队列,再执行一个宏任务,再清空微任务队列,循环往复。
- 微任务(Promise.then、MutationObserver、queueMicrotask)优先级高于宏任务(setTimeout、setInterval、setImmediate、I/O、UI 渲染)。
-
普通函数箭头函数题目核心区别:
-
this 指向:
- 普通函数:
this由调用方式决定(谁调用指向谁,new 绑定、call/apply 绑定优先级更高)。 - 箭头函数:没有自己的
this,继承外层作用域的this,且无法被 call/apply/bind 修改。
- 普通函数:
-
arguments:箭头函数没有
arguments对象,只能用 rest 参数...args。 -
prototype:箭头函数没有
prototype属性,不能作为构造函数,不能用new调用。 -
yield:箭头函数不能作为 Generator 函数,不能使用
yield。
典型题目示例:
javascript
运行
const obj = {
a: 1,
foo: function() {
console.log(this.a);
},
bar: () => {
console.log(this.a);
}
};
obj.foo(); // 普通函数,this指向obj → 输出1
obj.bar(); // 箭头函数,this继承外层(全局/module),非严格模式下this指向window,window.a为undefined → 输出undefined
const foo2 = obj.foo;
foo2(); // 普通函数,独立调用,this指向window → 输出undefined
44. 手写数组扁平化,再去用堆和栈的方法
- 递归实现:
javascript
运行
// 递归版,默认展开所有层级
function flatten(arr, depth = Infinity) {
return arr.reduce((acc, val) => {
if (Array.isArray(val) && depth > 0) {
return acc.concat(flatten(val, depth - 1));
}
return acc.concat(val);
}, []);
}
- 栈实现(DFS):
javascript
运行
function flattenWithStack(arr) {
const stack = [...arr];
const result = [];
while (stack.length) {
const item = stack.pop();
if (Array.isArray(item)) {
// 倒序入栈,保证顺序正确
stack.push(...item);
} else {
result.unshift(item);
}
}
return result;
}
- 队列实现(BFS):
javascript
运行
function flattenWithQueue(arr) {
const queue = [...arr];
const result = [];
while (queue.length) {
const item = queue.shift();
if (Array.isArray(item)) {
queue.unshift(...item);
} else {
result.push(item);
}
}
return result;
}