一句话总结
父子用 props/emit,双绑用 v-model,触发方法用 ref,跨级用 provide/inject,任意组件用 Pinia(或 mitt 事件总线),透传用 $attrs,内容分发用插槽。原则:通信层级越浅越好——超过两层还用 props 层层转递(props 钻透)就该换成 provide/inject 或状态管理。
初级理解
最基础的四种:
| 方式 | 方向 | 适用 |
|---|---|---|
| props | 父 → 子 | 传数据(单向数据流) |
| emit | 子 → 父 | 上报事件 |
| v-model | 双向 | 表单/弹窗显隐(语法糖) |
| ref + defineExpose | 父 → 子 | 调子组件方法/拿实例 |
中级深入
跨层级通信:provide/inject 祖先注入、任意后代消费。要传响应式数据,就 provide 一个 ref/computed,后代拿到的是同一个引用;直接 provide 普通值则是快照、不会同步。
插槽三种:默认插槽、具名插槽 v-slot:name、作用域插槽(子组件把内部数据传给插槽内容:<slot :row="item">)。作用域插槽让"结构由父定、数据由子给"成为可能,表格列自定义渲染就是这么实现的。
keep-alive:缓存组件实例(不销毁),切换时触发 activated/deactivated 而不是重新走 created/mounted。常用参数:include/exclude(按组件名匹配)、max(LRU 淘汰上限,防内存膨胀)。
全局事件总线:Vue3 移除了 $on/$off,$emit 不能再当总线用,社区方案是 mitt。事件总线"谁都能听、来源不可溯",只在无状态管理可用的轻量场景临时使用。
高级拓展
作用域插槽的编译本质:插槽内容会被编译成一个函数(接收子组件传来的参数返回 vnode)。子组件渲染时调用这个函数决定内容——这就是"作用域"的来源,也是它能随子组件数据更新的原因。默认插槽写在内、具名写在外时要注意 v-slot 只能用在 template 上。
$attrs 与透传:$attrs 收集了父组件传入但未声明为 props 的全部属性。深层级组件透传:v-bind="$attrs" 一行把属性接力下去;不希望根节点自动继承就设 inheritAttrs: false(Vue3 里再手动绑定到指定节点)。
mixin 的问题与 composable 替代:mixin 有三大缺点:属性来源不明确(出问题要翻好几个文件)、命名冲突、隐式依赖。组合式函数(useXxx)利用 ES Module 的显式导入导出解决了这三点:
实战场景
场景一:props 钻透(prop drilling)改造
场景二:弹窗组件的打开/关闭设计
场景三:列表页缓存与返回恢复
面试模拟
Q:provide/inject 和 Vuex/Pinia 怎么选?
A:看状态归属。provide/inject 适合"组件树内"的共享——比如主题、某个业务域的上下文,数据属于这棵子树;Pinia 适合"全局单例"状态——用户信息、权限、购物车。provide/inject 没有调试追踪,跨树使用会失控,所以要用 readonly 包住、只提供"读取接口"或方法。
Q:keep-alive 的缓存原理是什么?max 是怎么淘汰的?
A:keep-alive 是抽象组件,内部用 Map 以 key→vnode 缓存组件实例,渲染时命中缓存直接取回 DOM 不重新创建。max 用 LRU(最近最少使用)策略:命中就把 key 提到最新,超限时淘汰最久未用的缓存并调用其 unmount 钩子。
Q:为什么 Vue3 移除了 $on/$off(事件总线)?还怎么跨组件通信?
A:隐式事件总线让数据流变得不可追踪,和单向数据流的设计哲学冲突。替代方案:组件树内用 provide/inject 或状态管理共享状态;确实需要事件的轻量场景用 mitt;父子场景显式 emit。本质是把"隐式全局订阅"变成"显式依赖声明"。