左边是三层嵌套:Page → Panel → Leaf。Page 只 provide 一次,Panel 一行相关代码都不写,Leaf 直接 inject 就能用。
右边是逻辑复用:把「ref + 方法」打包成 useCounter、useLocalStorage 这样的组合式函数,谁需要谁调用,各拿一份独立状态。
一句话记住:provide 传 ref,值变了下游才变;provide 传普通数值 = 交出去一份拷贝,provide 普通对象(没套 reactive)也不会触发任何重新渲染 —— 它只是等别的响应式数据逼出一次重渲染时被顺带刷出来。
inject 一定写默认值:inject('missing', '没有这个 key,走默认值'),不写就只剩 undefined。
组合式函数就是 ref + 方法的打包,不是 Vue 的某种新对象:能在里面对外 provide,但也必须同步调用。
function useCounter(init) { // 一份状态 + 一组方法
const count = Vue.ref(init) // 每次调用都新建一份
const step = (n) => { count.value += n }
const reset = () => { count.value = init }
return { count, step, reset }
}
function useLocalStorage(key, fallback) { // 一份状态 + 自动落盘
const data = Vue.ref(fallback)
try { const s = localStorage.getItem(key)
if (s !== null) data.value = JSON.parse(s) } catch (e) {} // 读回来
Vue.watch(data, (v) => { // 变化就写回去
try { localStorage.setItem(key, JSON.stringify(v)) } catch (e) {}
})
const clear = () => { localStorage.removeItem(key); data.value = fallback }
return { data, clear, key }
}
| 约定 / 坑 | 怎么做 | 为什么 |
|---|---|---|
| 放哪、叫什么 | 文件放 src/composables/,一个文件一个函数,命名 useXxx(useCounter.js) |
它不是组件、不是插件,use 前缀就是「这是个带响应式的可复用逻辑」的记号 |
| 返回值用 ref,别用 reactive 整体 | return { count, step },调用方 const { count } = useCounter(0) 照常解构 |
返回一个 reactive 对象再解构,解构出来的是普通值,响应性当场丢 |
| 可以在里面对外 provide | function useTheme() { const t = ref('light'); provide('theme', t); return t } |
provide / inject 必须在组件实例上登记,所以只能在 setup 的同步过程里调 |
| 必须在 setup(或另一个 setup)同步期间调用 | 不要在 setTimeout、then、普通工具函数里调 inject / provide / watch |
那时已经没有「当前组件实例」了,inject 拿到 undefined,provide 无处可挂(点上面的按钮看现场) |
| 手段 | 方向 | 选择依据 | 典型场景 |
|---|---|---|---|
props / emit(示例 09) | 父子,一层一层 | 只有两层、且数据是「这个组件的输入」 | 列表项、表单项、弹窗 |
provide / inject | 祖先 → 任意深后代 | 中间层只是路过、不该替它搬箱子;或者一个组件要喂一整棵子树 | 主题、语言、表单实例、弹层容器 |
组合式函数 useXxx | 不是传值,是传逻辑 | 同样的「状态 + 行为」在两个以上组件里重复出现 | 计数器、本地存储、防抖输入、分页 |
| Pinia(示例 15) | 全局,任意组件平级读写 | 需要跨页面、跨无关分支共享,还要 devtools 时间旅行和可测试 | 登录用户、购物车、全局配置 |