Skip to content

腾讯面试题 ​

回答时先讲结论,再按 1、2、3 分点展开。涉及项目经历的内容,要换成自己真实做过的事情;没有实践过的方案,可以直接说“了解原理,还没有在项目中落地”。

1. 自我介绍 ​

  1. 我是谁:有多少年前端经验,主要用 Vue、React 还是小程序。
  2. 我做过什么:挑一个和岗位最相关的项目,说明项目做什么、自己负责什么。
  3. 我解决过什么问题:讲一个具体问题、采取的方法和结果,比如首屏慢、列表卡顿或公共组件难维护。有真实数据再说数据,不编数字。
  4. 我的优势是什么:结合上面的经历,说明自己擅长业务开发、性能优化还是工程建设,控制在一分钟左右。

可以这样组织:

我有【实际年限】年前端经验,主要使用【技术栈】。最近参与了【项目】,负责【模块】。其中遇到过【具体问题】,我通过【方法】改善了【实际结果】。我比较擅长【与岗位相关的能力】,希望继续在这个方向深入。

2. Vue 2 和 Vue 3 的区别 ​

  1. 响应式不同:Vue 2 主要给对象已有属性设置 getter、setter;Vue 3 的 reactive 使用 Proxy 代理对象,能拦截新增、删除属性等操作。
  2. Vue 2 有一些限制:给已观察的对象直接新增属性、通过下标修改数组元素,可能无法触发更新,需要用 $set、splice 等方式处理。
  3. 代码组织更灵活:Vue 3 常用 Composition API,把一个功能的数据、方法和监听放在一起,方便拆成组合式函数。Vue 3 仍支持 Options API,Vue 2.7 也支持 Composition API。
  4. 性能优化更多:Vue 3 会在编译时标记哪些地方可能变化,更新时减少不必要的比较;也更便于移除未使用的功能代码。不能直接说所有页面都固定快几倍。
  5. TypeScript 支持更自然:Vue 3 本身使用 TypeScript 开发,类型推断和组合式写法配合更方便。

3. Composition API 相比 Options API 的优势 ​

  1. 同一个功能可以写在一起:例如搜索功能的关键词、请求、加载状态,不用分散在 data、methods、watch 中来回找。
  2. 逻辑容易复用:可以抽成 useSearch、usePagination,多个组件直接使用,比 mixin 的数据来源更清楚。
  3. 类型更好推断:主要使用普通变量和函数,不依赖组件里的 this,更容易配合 TypeScript。
  4. 大组件更好拆分:可以按“搜索、表单、权限”等业务功能拆,而不是只按 Vue 的配置项拆。
  5. 不用一刀切:简单页面使用 Options API 也很清楚,选择时看复杂度和团队习惯。

4. Vue 2 新增属性除了 $set 怎么处理? ​

  1. 提前声明字段:如果已经知道会有哪些字段,直接在 data 中写好,初始值可以是 null 或空字符串。
  2. 替换整个对象:前提是 user 本身已经在 data 中声明。给它赋一个新对象,Vue 会观察这个新对象。
  3. 使用 Vue.set:它和 this.$set 是同一种处理方式的不同入口,不是另一套原理。
  4. 数组使用 splice:修改数组某一项时,可以用 this.list.splice(index, 1, newItem)。
  5. 不要把强制刷新当成修复:$forceUpdate 只是强制重新渲染,不会把遗漏的属性变成响应式属性。
js
// user 需要预先在 data 中声明
this.user = { ...this.user, age: 18 };

5. Pinia 相比 Vuex 有什么优势? ​

  1. 写法更简单:不用单独写 mutation,可以直接修改状态,也可以把业务修改统一放进 action。
  2. TypeScript 更方便:状态、getter、action 的类型通常可以自动推断,减少手动声明。
  3. 拆分更自然:用户、购物车、权限可以各自一个 store,按需引入。
  4. 支持调试:可以结合开发工具查看状态变化,也支持插件扩展。
  5. 全局状态不要滥用:多个页面或组件确实要共享的数据再放进 store。持久化需要额外实现,Pinia 不会自动把数据存到本地。

6. 不用第三方库怎么做简单全局状态? ​

  1. 在单独的模块里创建状态:把 reactive 放在函数外面,多个组件导入时拿到的是同一份数据。
  2. 提供统一的修改方法:对外暴露只读状态和操作函数,避免每个组件都随意改。
  3. 小范围共享用 provide/inject:例如只在某个页面及其子组件之间共享,不一定需要全局变量。
  4. 复杂后再考虑 Pinia:需要规范化调试、插件、多个 store 协作时,成熟的状态库更省维护成本。
  5. 服务端渲染要隔离请求:下面这种模块级单例适合客户端应用。SSR 里每个请求需要自己的状态,不能让不同用户共享一份数据。
js
import { reactive, readonly } from 'vue';

const state = reactive({ user: null });

export function useAppState() {
  const setUser = user => {
    state.user = user;
  };

  return { state: readonly(state), setUser };
}

7. 解构和展开为什么可能破坏响应式? ​

  1. 基本类型解构后只是一个值:从响应式对象取出字符串、数字,相当于拍了一张快照,后续原属性变化不会同步到这个变量。
  2. 用 toRef、toRefs 保持关联:得到的是指向原属性的 ref,原属性更新,读取这个 ref 也能获得新值。
  3. 展开只是浅拷贝:{ ...state } 创建了新的外层对象,不会自动跟随原对象的顶层属性变化。
  4. 不是所有解构都失去响应式:如果解构出来的是一个响应式嵌套对象,修改这个对象内部属性仍可触发更新;但原属性改指向另一个对象时,旧变量不会自动跟过去。
  5. 编译器处理是例外:Vue 3.5 起,在同一个 <script setup> 中直接解构 defineProps(),编译器会对相关访问进行响应式转换,不能和普通对象解构混为一谈。
js
import { reactive, toRefs } from 'vue';

const user = reactive({ name: '小明' });
const { name: snapshot } = user; // 普通字符串
const { name } = toRefs(user); // 与 user.name 保持关联

user.name = '小红';
console.log(snapshot); // 小明
console.log(name.value); // 小红

参考:Vue 响应式原理。

8. watch 和 watchEffect 的原理及异步问题 ​

  1. watch 明确指定监听谁:例如监听关键词,变化后执行请求;默认不会立即执行回调,可以设置 immediate: true。
  2. watchEffect 自动收集依赖:会先执行一次,执行过程中读取到哪些响应式数据,就建立对应的监听关系。
  3. 底层都是收集依赖再触发更新:读取响应式数据时记录依赖,数据变化后通知相关任务执行。watch 的回调里随便读取的其他数据,不会自动成为它的监听源。
  4. 异步依赖有边界:watchEffect 每次执行时,只追踪第一个 await 之前同步读取的依赖;只在 await 后访问的数据不会被自动追踪。
  5. 防止旧请求覆盖新结果:关键词变化时取消上一次请求,或者给请求编号,只接收最后一次结果;还要处理失败、取消和加载状态。
  6. 要读更新后的 DOM:可以使用 flush: 'post'。不要默认认为监听回调执行时页面已经更新好了。

参考:Vue 侦听器。

9. Observer 对象是什么? ​

  1. 先确认面试官说的是哪一种:浏览器的观察器 API,还是 Vue 2 源码中的 Observer。
  2. IntersectionObserver:观察元素与可视区域是否相交,常用于图片懒加载、曝光统计。
  3. ResizeObserver:观察元素尺寸变化,常用于图表自适应、容器布局调整。
  4. MutationObserver:观察 DOM 节点、属性、文本的变化,不是专门用来观察尺寸的。
  5. Vue 2 的 Observer:负责观察对象和数组,配合 getter、setter 及数组方法处理,让数据变化能通知依赖更新。
  6. 记得清理:浏览器观察器不再使用时调用 disconnect(),避免继续处理无用的回调。

10. 隐私接口加密和密钥管理 ​

  1. 传输首先使用 HTTPS:减少数据在传输途中被窃听或篡改的风险。
  2. 真正的秘密放服务端:私钥、数据库密码、第三方服务密钥不能打包进前端。前端环境变量和代码混淆都藏不住秘密。
  3. 每个接口都要鉴权:服务端检查“你是谁、能访问谁的数据”。不能因为请求做了加密,就跳过权限检查。
  4. 确实需要字段加密时再设计方案:例如使用临时 AES 密钥加密数据,再使用服务端公钥加密 AES 密钥。公钥可以公开,私钥留在服务端,具体算法和密钥管理交给成熟方案。
  5. 防重放需要服务端配合:服务端校验时间窗口、一次性标识并拒绝重复请求;只在请求里加一个时间戳不够。
  6. 注意数据使用过程:日志脱敏、按需展示敏感字段、轮换密钥。客户端最终需要显示的明文,不能靠传输加密对客户端使用者隐藏。

11. Git submodule 怎么优化? ​

  1. 先说它是什么:主仓库记录另一个仓库的地址和具体提交,适合让公共代码保持独立版本。
  2. 先保证拉取方式正确:克隆时使用 git clone --recurse-submodules;已有仓库使用 git submodule update --init --recursive。
  3. 固定版本保证可复现:更新子模块后,要提交主仓库中记录的子模块提交号。不要构建时总是追最新分支。
  4. 优化下载和协作:只需要当前版本时可考虑浅克隆;CI 可以缓存下载,并明确子模块更新、联调、回滚的流程。
  5. 公共库可以改成 npm 包:如果主要是共享组件、工具函数,发布包通常比直接共享源码更方便管理版本。
  6. 多个项目频繁联动可考虑 monorepo:统一放在一个仓库管理,但要评估构建、权限和迁移成本,不是所有 submodule 都要替换。

12. 富文本怎么准确统计字符? ​

  1. 先定统计规则:空格、换行算不算?图片算几个字符?表格、提及用户怎么统计?这些需要先和产品确认。
  2. 从编辑器的数据模型读取内容:优先用编辑器提供的文档结构和 API,不要用 innerHTML.length,否则 HTML 标签也会算进去。
  3. 按用户看到的字符统计:JavaScript 的 length 计算的是 UTF-16 代码单元,一个 emoji 可能占多个。可以用 Intl.Segmenter 按字素分段。
  4. 特殊节点单独处理:图片、表格、mention 按约定计算,不要既统计节点又重复统计内部文字。
  5. 大量内容避免每次全量遍历:可以按变更的段落更新计数;提交时服务端再按同样规则校验。
js
const segmenter = new Intl.Segmenter('zh-CN', {
  granularity: 'grapheme'
});

const count = [...segmenter.segment('你好👨‍👩‍👧‍👦')].length;
console.log(count); // 3,家庭 emoji 作为一个字素

13. Vite 相比 Webpack 的优势 ​

  1. 开发启动通常更快:Vite 利用浏览器原生 ES 模块能力,开发时按需处理源码,避免传统整包构建等待。
  2. 改代码反馈快:热更新时尽量只更新受影响的模块,大项目开发体验通常更好。
  3. 常见功能开箱即用:开发服务器、模块加载、常见资源处理等已有默认配置。
  4. 构建工具要结合版本说:常见旧版本 Vite 的生产构建使用 Rollup;Vite 8 已切换到 Rolldown。面试中应说明自己项目实际使用的版本。
  5. Webpack 仍有适用场景:已有复杂 loader、插件和成熟构建流程的项目,不一定值得马上迁移;需要评估兼容性和实际收益。

参考:Vite 8 迁移说明。

14. 微前端原理,为什么要选用微前端? ​

  1. 把一个大应用拆成多个子应用:例如订单、商品、运营分别开发,再由主应用组合成一个系统。
  2. 主应用负责协调:通常管理入口、菜单、路由和子应用加载;子应用负责自己的页面和业务逻辑。
  3. 运行时按需挂载和卸载:进入某个业务路由时加载对应子应用,离开时清理事件、定时器等资源。
  4. 重点是隔离与通信:处理样式冲突、全局变量污染和依赖冲突;通过约定的接口、事件或共享状态传数据。iframe 隔离强,但页面融合和通信更麻烦。
  5. 适合团队和发布需要独立的情况:多个团队维护大系统,或者旧系统要逐步迁移,可以让子应用独立开发和发布。
  6. 代价也要讲清楚:路由、登录态、依赖版本、性能和排错都会更复杂。小团队的小项目通常没必要使用。代码沙箱也不能一概当成安全边界。

15. 小程序怎么实现动画,帧动画怎么实现? ​

  1. 简单过渡用样式:例如淡入淡出、缩放、位移,可以用 WXSS 的 transition 或 animation。
  2. 需要代码控制时用动画 API:例如 wx.createAnimation,设置动作并导出动画数据绑定到组件;实际能力要结合基础库和渲染模式确认。
  3. 帧动画就是连续切换画面:把多张静态图快速依次显示,看起来就像在运动。
  4. 雪碧图是常见实现:把各帧拼在一张图中,用固定大小的窗口显示,通过关键帧和 steps() 逐帧改变背景位置,避免平滑滑动成“拉图”效果。
  5. 也可以使用 Canvas 绘制:预加载帧图片后按时间绘制下一帧,适合需要精细控制的情况。
  6. 注意性能与清理:优先使用 transform、opacity,避免用高频 setData 驱动每一帧;页面隐藏或卸载时停止动画和定时器。

16. 图片预加载怎么实现? ​

  1. 预加载就是提前请求图片:用户还没看到下一张图时,就先下载,减少切换时的等待。
  2. 网页可以使用 new Image():先注册加载和失败回调,再设置 src。之后使用相同地址时,浏览器可能复用缓存,具体取决于缓存策略。
  3. 首屏关键图片可以声明 preload:使用 <link rel="preload" as="image">,但只预加载重要资源,避免抢占其他资源带宽。
  4. 只提前加载接下来可能用到的图:轮播可以提前加载后面一两张,不要把整个图库一次性下载。
  5. 区别懒加载:预加载是提前取;懒加载是快进入可视区域时才取。两种方式解决的场景不同。
  6. 小程序没有浏览器的 Image 构造器:需要结合图片组件、wx.getImageInfo 或下载能力处理,不能直接照搬网页代码。
js
// 浏览器环境示例
function preloadImage(src) {
  return new Promise((resolve, reject) => {
    const image = new Image();
    image.onload = () => resolve(image);
    image.onerror = () => reject(new Error(`图片加载失败:${src}`));
    image.src = src;
  });
}

17. 小程序怎么实现虚拟列表? ​

  1. 只渲染屏幕附近的数据:即使有一万条,也只生成当前能看到的几十条节点,减少渲染和内存压力。
  2. 根据滚动位置计算范围:如果每项高度固定,起始下标大致是 Math.floor(scrollTop / itemHeight),再根据容器高度算出需要多少条。
  3. 用空白区域撑起完整高度:上方占位高度是起始下标乘每项高度,下方也按剩余项数占位,让滚动条长度正常。
  4. 前后多渲染一些作为缓冲:避免快速滚动时出现空白;滚动事件里只在可见范围变化时更新,减少 setData 次数和数据量。
  5. 不定高更复杂:需要测量、缓存每项高度,计算累计高度,再定位可见范围;图片加载后高度变化也要处理。
  6. 不要把分页当成虚拟列表:分页减少每次请求的数据;虚拟列表减少页面上实际渲染的节点,两者可以一起使用。scroll-view 本身不代表已经实现虚拟列表。

参考:微信 recycle-view 的实现思路。

18. 小程序消息订阅:一次性和长期订阅有什么区别? ​

  1. 都需要用户同意:通常在用户点击“订阅提醒”等操作后调用 wx.requestSubscribeMessage,不能把进入页面当成默认同意。
  2. 一次性订阅:用户对某个模板同意一次,通常获得一次对应消息的下发机会;再次发送需要有可用的订阅次数。
  3. 长期订阅:符合条件的场景中,用户同意后可按平台规则多次发送,不是每次都重新申请。
  4. 长期订阅并非所有小程序都能用:平台对类目、模板和场景有限制,以小程序后台实际开放的权限和模板为准。
  5. 消息由服务端发送:前端记录用户对各模板的接受或拒绝结果,服务端在订单、预约等业务事件发生时调用发送接口,并处理失败和重复发送。
  6. “记住选择”不等于长期订阅:它影响后续授权交互,不代表一次性模板变成了无限发送权限。用户拒绝后也不要反复强迫授权。

19. 小程序进来怎么实现默认登录? ​

  1. 先检查自己的业务登录态:本地有 token 时,交给服务端验证是否仍有效;本地有值不代表一定登录成功。
  2. 没有有效登录态就调用 wx.login:拿到临时 code 后发给自己的后端。code 不是业务 token,也不是用户资料。
  3. 后端向微信换取身份信息:后端使用 code 调用 code2Session,获取 openid、session_key 等;满足条件时才有 unionid。
  4. 后端建立自己的会话:查找或创建业务用户,签发业务 token 给前端。AppSecret 和 session_key 留在服务端。
  5. 不要混淆两种会话:wx.checkSession 检查微信登录态,不能代替后端检查业务 token;过期后按对应流程重新登录。
  6. 静默登录不等于拿到全部资料:手机号、头像昵称等需要按对应能力和用户操作获取,不能默认静默读取。

20. AI 使用的流程 ​

这里按“在开发中如何使用 AI”回答。

  1. 先明确任务:说清要解决什么问题、输入是什么、最后要输出什么,以及哪些内容不能改。
  2. 给必要上下文:提供相关代码、接口说明、错误信息和技术版本,不要只说“帮我优化一下”。
  3. 先确认方案再分步实现:复杂需求可以先让 AI 拆步骤,再逐块修改,避免一次生成大量难以检查的代码。
  4. 检查结果:核对 API 是否真实、逻辑是否正确,运行构建和必要测试;页面修改还要实际打开看交互。
  5. 根据实际错误继续迭代:把日志、复现步骤和预期结果反馈给 AI,不只说“还是不行”。
  6. 最后由自己负责交付:检查差异、保护密钥和用户数据,重要变更经过评审。面试时最好举一个真实案例,说明 AI 帮了哪一步、自己验证了什么。

21. AI 的知识图谱怎么使用,原理是什么? ​

  1. 知识图谱就是“实体加关系”:把人、项目、产品等看成点,把“负责”“依赖”“属于”等关系看成连接线。例如“小王负责订单系统,订单系统依赖支付服务”。
  2. 先从资料里提取信息:读取文档、数据库等内容,识别实体和关系,合并同名实体,同时保留原文来源。可以让模型辅助提取,但提取结果要校验。
  3. 查询时沿关系找答案:问“支付服务故障影响哪些系统”,就沿“依赖”关系寻找关联节点,比只匹配“支付”这个词更适合多层关系问题。
  4. 和大模型结合时,先检索再生成:先找相关实体、关系和原文,把这些资料提供给模型,让它据此组织回答,这是一种图谱辅助的 RAG 思路。
  5. 和普通向量检索可以配合:向量检索擅长找语义相似的段落,图谱擅长找明确的关联路径;可以先找文档,再扩展相关关系,最后结合原文回答。
  6. 图谱不会自动让答案绝对正确:关系提取可能出错,资料可能过时,还要处理访问权限、来源追溯、数据更新,以及回答质量验证。建立图谱也不等于重新训练模型。

22. 防抖和节流的区别 ​

  1. 防抖是“等你停下来再执行”:连续触发时,每次都重新计时;最后一次触发后,等满指定时间才执行。适合搜索框输入结束后发请求。
  2. 节流是“持续触发也控制执行频率”:例如每 200 毫秒最多处理一次,不会因为事件频繁就一直执行。适合滚动位置计算、鼠标移动等场景。
  3. 用搜索框举例:用户不断输入,防抖会等输入暂停后再搜索;节流则可能在输入过程中隔一段时间搜索一次。
  4. 第一次和最后一次是否执行,要看配置:常见实现有立即执行、结束后执行,或两者结合。不能简单说防抖永远不立即执行、节流永远不处理最后一次。
  5. 防抖不能解决所有异步问题:搜索请求已经发出去后,清除定时器不会取消请求。还要取消旧请求,或者用请求编号避免旧结果覆盖新结果。
  6. 组件销毁时清理:清除尚未执行的定时器,移除事件监听;封装时保留函数参数和 this,不要在每次事件触发时重新创建防抖函数。

可以直接这样回答:

防抖是等操作停下来再执行,节流是操作不停也按固定频率执行。搜索输入常用防抖,滚动处理常用节流,具体还要看是否需要立即执行和保留最后一次结果。

23. OPTIONS 和跨域是什么关系? ​

  1. 跨域先看是否同源:协议、主机、端口有一个不同,就是不同源;只有路径不同不算跨域。例如两个不同子域名也属于跨域。
  2. 浏览器有限制,服务器可以通过 CORS 允许读取:服务器返回相应响应头,告诉浏览器哪些来源可以读取响应。前端自己加一个允许跨域的请求头没有用。
  3. OPTIONS 是一种 HTTP 方法,预检会用它:浏览器发送某些跨域请求前,会先问服务器:“这个来源能不能用这个方法、带这些请求头访问?”但不是所有 OPTIONS 都是跨域预检。
  4. 不是跨域就一定预检:满足安全列表条件的请求可以直接发送;使用 PUT、DELETE、自定义请求头或 Content-Type: application/json 等,通常会触发预检。GET 也可能因为带了非安全列表请求头而预检。
  5. 请求头和响应头不要记反:预检请求带 Origin、Access-Control-Request-Method,必要时带 Access-Control-Request-Headers;服务器用 Access-Control-Allow-Origin、Access-Control-Allow-Methods 等响应头表明是否允许。
  6. 携带 Cookie 需要额外配合:跨源 fetch 通常设置 credentials: 'include',服务端允许凭证并返回具体来源,不能用 *;Cookie 自身的 SameSite 等规则仍会生效。
  7. 实际项目通常用 CORS 或同源代理解决:开发代理只在开发环境生效,线上要配置后端或网关。CORS 不是身份验证,也不保证请求没有到达服务器,不能代替鉴权和 CSRF 防护。

可以直接这样回答:

跨域是请求的来源和目标不同源。浏览器对读取响应有限制,需要服务端通过 CORS 允许。对于不满足简单请求条件的情况,浏览器会先发 OPTIONS 预检,确认允许后再发实际请求,实际响应也要满足 CORS 要求。

参考:MDN CORS。

24. 强缓存和协商缓存的区别 ​

  1. 强缓存是“缓存还新鲜,直接用”:在缓存允许且未过期的情况下,浏览器可以直接使用本地内容,不必再请求服务器。常见控制头是 Cache-Control: max-age=3600,表示新鲜期为一小时。
  2. 协商缓存是“问服务器,这份还能用吗”:本地有缓存但需要验证时,浏览器发送条件请求,由服务器判断内容有没有变化。
  3. 两组常见校验字段:服务器返回 ETag,浏览器下次带 If-None-Match;或者服务器返回 Last-Modified,浏览器下次带 If-Modified-Since。两种条件同时提供时,优先判断 If-None-Match。
  4. 没变通常返回 304,变了通常返回 200:304 不携带资源正文,浏览器使用已有缓存;200 则返回新的正文。协商缓存仍有网络请求,不能说它完全不耗时。
  5. no-cache 和 no-store 不一样:no-cache 可以保存,但复用前需要验证;no-store 要求不要存储该响应。不要把 no-cache 解释成“完全不缓存”。
  6. 两者可以配合:先按新鲜期直接使用,过期后再用 ETag 等验证;如果没有可用的校验信息,也可能直接重新下载。
  7. 项目中按资源配置:带内容哈希的 JS、CSS 适合长缓存,内容变化时更换 URL;HTML 可以设置重新验证,及时拿到新资源地址。缓存问题还要区分浏览器缓存和 CDN 缓存。

可以直接这样回答:

强缓存命中时直接用本地内容,不用问服务器;协商缓存要发请求确认,没变化返回 304,继续用本地内容。项目里常让带哈希的静态资源长缓存,让 HTML 及时验证更新。

参考:MDN HTTP 缓存。