二面追问
第一部分:项目深挖(工程化 + 软技能)
1. 项目中遇到的最大技术难点是什么?
基础认知:难点分两类——技术实现难(不会做)和方案选型难(怎么做更好)。
进阶回答(结合你的IRMP + IAMP全流程打通):
“最大的难点不是单个技术点,而是**‘数据孤岛’的打通**。检测车采集的点云数据(IAMP)、监控量测的传感器数据(MMMP)、最终出具的PDF报告(IRMP)原本是三个独立的系统,数据格式不统一。我的难点在于设计统一的数据中转层(BFF层),用Node.js做聚合,前端根据业务场景自动切换数据源。 具体解法:设计了一套统一的数据适配器(Adapter),将点云坐标、传感器数值、报告元数据统一映射为
{ timestamp, value, type }标准格式。前端大屏只需要消费这一套标准数据,新增设备时只需增加适配器,无需改动大屏代码。 结果:新设备接入时间从2周缩短至2天,系统扩展性大幅提升。”
2. 如何保证代码质量和可维护性?
基础认知:ESLint、Prettier、Code Review。
进阶回答(展现架构师思维):
“我在团队推行了‘三层质量防线’:
- 编码阶段(自动化):配置
husky+lint-staged,提交代码时自动格式化并运行单元测试(Jest/Vitest),不合格代码进不了仓库。 - 合并阶段(人工):强制 MR(Merge Request)必须有2位同事 Approve,重点审查业务逻辑和是否复用已有组件。
- 长期维护(文档):我在 WeLink 维护 SuitUI 组件库时,强制要求每个公共组件必须写 Storybook 示例和
README.md(含Props表格)。在IRMP项目中,我把复杂的PDF生成逻辑抽成独立的report-engine包,发布到公司私有 NPM 仓库,版本号严格遵循语义化(Semver)。”
- 编码阶段(自动化):配置
量化数据:推行后,线上Bug率下降40%,新人接手项目熟悉周期从1个月缩短至1周。
3. 团队协作中如何处理技术分歧?
基础认知:沟通、妥协、数据说话。
进阶回答(经典的STAR法则):
“在MMMP项目中,关于视频流方案,后端同事坚持用 HLS(延迟2-3秒) ,因为他们熟悉;但我坚持 WebRTC(延迟<500ms) 因为业务要求实时监控。 我的处理方式:
- 不争论,发起 POC(概念验证) 小会,花一天时间分别搭出两套Demo。
- 拉上产品经理一起做压力测试(模拟工地弱网环境),数据证明WebRTC在打洞成功率80%情况下延迟远优于HLS。
- 提出折中方案:默认用WebRTC,但封装一层降级开关,网络差时自动降级HLS。
结果:方案全票通过,且我主动把降级逻辑封装成公共组件,其他项目直接复用。”
4. 有没有做过性能监控?指标有哪些?
基础认知:前端监控体系。
进阶回答(结合WeLink和你的项目):
“在 WeLink IM 模块,我接入过公司的自研监控平台,重点关注以下指标:
- Web Vitals(核心体验指标):LCP(最大内容绘制,<2.5s)、FID(首次交互延迟,<100ms)、CLS(布局偏移,<0.1)。
- JS错误率:通过
window.onerror和unhandledrejection捕获,按errorType聚合告警。 - 接口成功率:拦截
axios响应,监控/sendMessage等核心接口的HTTP状态码和耗时。
在IRMP平台,我自定义了**‘报告生成耗时’**埋点,通过
performance.mark记录从点击到PDF下载完成的耗时,发现P50是12s,P95是35s。基于此数据,我推动了后端优化(异步队列),将P95降低至18s。”
第二部分:Vue 框架核心深度(二面必考)
1. Vue响应式原理是什么?
(快速回顾,你之前已经深度准备了track/trigger,这里直接精简有力地抛出来)
- 回答:“Vue2用
Object.defineProperty递归遍历,Vue3用Proxy配合targetMap做懒代理。核心是track收集依赖(存进WeakMap -> Map -> Set),trigger派发更新。Vue3最大的进步是支持了deleteProperty和新增属性的响应式,且初始化性能提升了至少50%。”(如果面试官追问targetMap结构,直接把之前那三层图画出来)。
2. computed 和 watch 的区别?(高频易混淆)
基础认知:
computed:计算属性,依赖其他数据,有缓存(依赖不变,直接返回缓存值)。watch:侦听器,侦听特定数据,数据变化时执行副作用(回调函数),无缓存。
源码级深度(必杀技):
computed本质上是一个特殊的effect,设置了lazy和scheduler。它维护一个_dirty标志位,只有依赖变化时才重新计算(缓存机制)。watch底层是doWatch,通过遍历source触发getter收集依赖,变化时执行用户回调,支持immediate(立即执行)和deep(深度监听)。
实战选择(结合你的项目):
“在IRMP报告列表页,我需要根据筛选条件动态计算表格数据,我用
computed缓存过滤后的列表。而监听路由参数变化重新请求数据,我用watch(配合immediate: true)。”
3. 组件通信方式有哪些?(尽量说全)
- 基础:
props/$emit(父子) - 跨层级:
provide / inject(爷孙,注意响应式要传函数或reactive对象) - 全局:
Vuex/Pinia(状态管理) - 事件总线:
mitt(替代Vue2的$on,用于任意组件通信,但不推荐滥用) - 特殊:
$refs/$parent(直接调用子组件方法或属性) - 结合你的简历:“在IAMP平台,我使用
provide/inject向深层子组件注入Three.js场景实例,避免层层传递props。”
4. 路由守卫的使用场景?
基础分类:
- 全局守卫(
beforeEach、afterEach) - 路由独享守卫(
beforeEnter) - 组件内守卫(
beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave)
- 全局守卫(
实战场景(结合华为内审中心AI小助理):
“我在内审中心项目用
router.beforeEach做动态权限拦截——从 Pinia store 读取用户角色,如果是‘普通用户’访问‘管理员页面’,直接next('/403')。另外在 IRMP 报告编辑页,用户可能误触关闭浏览器,我用beforeRouteLeave监听未保存状态,弹出确认框‘内容未保存,确定离开吗?’。”
5. Vuex / Pinia 的状态管理流程?(必画图)
基础认知(画图解释):
- Vuex:
View -> dispatch -> Actions (异步) -> commit -> Mutations (同步) -> State -> View。强调Mutations必须是同步函数。 - Pinia:取消了
Mutations,只有State、Getters、Actions(同时支持同步和异步)。更扁平,且完美支持 TypeScript(自动类型推断)。
- Vuex:
源码级深度:Pinia 的
store本质上是reactive对象。访问store.count触发get收集依赖,修改store.count = 1触发set和trigger(直接利用了Vue3的响应式)。结合你的项目:
“IRMP 项目我从 Vue2(Vuex)升级到 Vue3(Pinia)。原本用 Vuex 管理‘用户信息’、‘报告列表’、‘权限菜单’,模块多且繁琐。切 Pinia 后,我拆成三个
defineStore(useUserStore、useReportStore、usePermissionStore),代码量减少 40%,配合storeToRefs解构赋值,组件内调用极其丝滑。”
第三部分:手撕代码 —— 二叉树的层序遍历(BFS)
题目要求:给一个二叉树,返回其按层序遍历得到的节点值(即逐层地,从左到右访问所有节点)。通常要求输出 [[root], [level1], [level2]] 这样的二维数组。
- 基础认知:BFS(广度优先搜索)依赖于**队列(Queue)**数据结构(JS中用数组模拟,
push+shift)。 - 进阶要点(面试官想看的):
- 不仅要写出来,还要说清楚时间复杂度 O(n) 和 空间复杂度 O(n)。
- 要能在循环中正确区分每一层(通过记录每层长度
levelSize)。 - 处理边界情况(
root为空)。
手写完整代码(背这个,一次过):
class TreeNode {
constructor(val, left = null, right = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function levelOrder(root) {
// 【边界处理】面试必写!鲁棒性体现
if (!root) return [];
const result = [];
const queue = [root]; // 初始化队列,放入根节点
// BFS 主循环
while (queue.length > 0) {
// 【核心亮点】这里先记录当前队列的长度,即当前层的节点总数
const levelSize = queue.length;
const currentLevel = [];
// 只循环当前层的节点(注意:queue.length 在循环中会变化,所以必须用 levelSize)
for (let i = 0; i < levelSize; i++) {
// 取出队首节点
const node = queue.shift();
// 处理当前节点(存入当前层结果数组)
currentLevel.push(node.val);
// 将下一层的子节点按顺序推入队尾(先左后右)
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
// 当前层遍历完毕,将结果存入最终数组
result.push(currentLevel);
}
return result;
}
// --- 测试用例 (方便你验证) ---
// 构造一棵树: 1 -> (2, 3) -> (4, 5, 6, 7)
const tree = new TreeNode(1,
new TreeNode(2, new TreeNode(4), new TreeNode(5)),
new TreeNode(3, new TreeNode(6), new TreeNode(7))
);
console.log(levelOrder(tree));
// 输出: [[1], [2, 3], [4, 5, 6, 7]]面试进阶追问(防卷提示): 如果面试官问“如果不用 shift(因为 shift 时间复杂度是 O(n)),怎么优化?”,你可以立刻回答:
“用两个数组模拟队列交换,或者用索引指针(
let head = 0,通过queue[head++]取值,避免shift带来的数组重排开销。” 手动实现优化版(展示你懂底层):
function levelOrderOptimized(root) {
if (!root) return [];
const result = [];
const queue = [root];
let head = 0; // 指针,替代 shift
while (head < queue.length) {
const levelSize = queue.length - head;
const currentLevel = [];
for (let i = 0; i < levelSize; i++) {
const node = queue[head++]; // 利用指针取出,不清除数组
currentLevel.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
result.push(currentLevel);
}
return result;
}二面综合备战建议
- 软技能要“讲故事”:回答“团队分歧”和“代码质量”时,不要说“我觉得应该怎样”,要说“我牵头拉了一次会议,做了几页PPT/POC,用数据证明了我的方案,最终达成了共识”。这符合华为“力出一孔,用事实说话”的价值观。
- 先画图再写算法:拿到二叉树算法题时,先在纸上画一棵简单的树(比如
1->2->3),标明队列每一步的变化。面试官很看重你解决问题的思路可视化能力。 - 把二面所有Vue问题连起来:当问到
computed时,主动带出watch的对比;当问到组件通信时,主动带出provide/inject和Pinia的区别。展现你对Vue全家桶的体系化认知。