Vue3 面试题:响应式、组件、渲染、Diff 与编译器
Vue3 面试常从 API 用法切到源码原理:响应式如何追踪依赖、组件如何更新、虚拟 DOM 如何 diff、编译器做了什么优化。
响应式系统
Proxy、track 与 trigger
知识点讲解
Vue3 用 Proxy 拦截对象读取和修改。读取时通过 track 收集当前副作用函数,修改时通过 trigger 通知相关副作用重新执行。依赖关系通常可以理解成 target -> key -> effects。
面试常问
问:Vue3 为什么用 Proxy 替代 Object.defineProperty?
答:Proxy 能拦截新增、删除、数组索引、Map/Set 等更多操作,不需要递归劫持所有属性,能力更完整。
ref、reactive 与 computed
知识点讲解
reactive 用于对象响应式,ref 用于包装任意值,模板里会自动解包。computed 有缓存,只有依赖变化才重新计算,适合派生状态。
面试常问
问:computed 和 watch 区别?
答:computed 用来声明派生值,有缓存,强调“算出一个值”;watch 用来监听变化并执行副作用,比如请求接口、操作缓存。
组件系统
props、emit 与 slot
知识点讲解
props 是父传子,emit 是子通知父,slot 是内容分发。组件设计时要保持单向数据流,子组件不要直接修改父组件传入的 props。
面试常问
问:为什么不能直接修改 props?
答:props 来源于父组件,直接修改会破坏单向数据流,也可能在父组件重新渲染时被覆盖。应该通过 emit 通知父组件修改。
provide/inject
知识点讲解
provide/inject 适合跨层级传递依赖,比如主题、表单上下文、组件库内部状态。它不适合替代全局状态管理,因为来源不如 store 清晰。
面试常问
问:provide/inject 是响应式的吗?
答:如果 provide 的是响应式对象或 ref,inject 后仍然保持响应式;如果提供普通值,则不会自动变成响应式。
渲染与更新
虚拟 DOM
知识点讲解
虚拟 DOM 是用 JS 对象描述真实 DOM。状态变化后生成新的 vnode,再通过 diff 找出最小必要更新。它带来跨平台能力和声明式渲染模型。
面试常问
问:虚拟 DOM 一定比原生 DOM 快吗?
答:不一定。虚拟 DOM 的优势是可维护性、跨平台和批量更新策略。手写极致优化 DOM 可能更快,但工程复杂度更高。
Diff 与 key
知识点讲解
Vue3 对同层节点做 diff。列表更新时 key 用来标识节点身份,帮助复用 DOM 和组件实例。没有稳定 key 时,可能导致状态错乱或更新低效。
面试常问
问:为什么不建议用 index 做 key?
答:列表插入、删除、排序时 index 会变化,Vue 可能错误复用节点,导致输入框状态、组件内部状态错乱。稳定唯一 id 更合适。
编译器与生态
template compiler
知识点讲解
Vue 模板会被编译成 render 函数。编译阶段可以做静态提升、patch flag、缓存事件处理函数等优化,减少运行时 diff 成本。
面试常问
问:patch flag 是什么?
答:它是编译器给动态节点打的标记,告诉运行时哪些部分可能变化。这样更新时不用全量比较,提高性能。
Pinia、Router 与 Nuxt
知识点讲解
Pinia 是 Vue 官方推荐状态管理,API 更轻、更贴近 Composition API。Vue Router 负责路由匹配和导航守卫。Nuxt 提供约定式路由、SSR、SSG、数据获取和部署能力。
面试常问
问:什么时候用 Pinia?
答:跨组件共享、需要持久化、需要 devtools 调试、多个页面共同依赖的状态适合放 Pinia。局部组件状态仍然用 ref 或 reactive。
KeepAlive、Teleport 与 SSR
知识点讲解
KeepAlive 缓存组件实例,适合 Tab 和列表详情返回;Teleport 把组件渲染到 DOM 其他位置,适合弹窗;SSR 在服务端生成 HTML,改善首屏和 SEO。
面试常问
问:SSR 要注意什么?
答:服务端没有 window 和 document,副作用要放到客户端生命周期里;还要注意状态隔离,避免不同请求共享同一份全局状态。
底层追问与代码示例
手写一个最小响应式系统
知识点讲解
Vue3 响应式的核心可以简化为三件事:读取时收集依赖,修改时触发依赖,副作用函数重新执行。
1 | const bucket = new WeakMap() |
真实 Vue 还要处理嵌套 effect、调度器、依赖清理、computed 懒执行、数组和集合类型、只读代理等复杂情况。
computed 的缓存原理
知识点讲解
computed 本质是懒执行的 effect。依赖不变时返回缓存值;依赖变化时只标记 dirty,等下次读取再重新计算。
1 | function computed(getter) { |
面试时可以说明:真实 Vue 的 computed 会通过 scheduler 在依赖变化时触发 dirty 标记,并在被其他 effect 读取时继续建立依赖关系。
Diff 中 key 的底层意义
知识点讲解
key 不是为了消除 warning,而是给 vnode 一个稳定身份。没有 key 时框架只能按位置复用;有 key 时可以判断节点是否移动、删除、插入。
1 | <template> |
如果使用 index 做 key:
1 | <div v-for="(user, index) in users" :key="index"> |
当列表头部插入新用户时,旧 DOM 会按位置复用,输入框内部状态可能错位。
面试常问
问:Vue3 Diff 为什么会提到最长递增子序列?
答:在有 key 的列表 diff 中,Vue 会找出新旧节点中可以复用且相对顺序不变的最长递增子序列。这部分节点不用移动,其他节点再移动或插入,从而减少 DOM 操作。
watch 的 flush 时机
知识点讲解
watch 默认在组件更新前后有调度时机差异,常见配置有 pre、post、sync。
1 | watch(source, callback, { |
post 适合在 DOM 更新后读取 DOM 状态;sync 会同步触发,容易造成频繁执行,谨慎使用。