讲解
props 传值有个痛点:隔代传递。祖父组件的数据要给深层子孙用,中间三四层组件根本不需要这个数据,却都得声明 prop 转手——这叫 prop drilling,又啰嗦又脆弱(中间层重构就断链)。provide/inject 专治这个病:祖先组件 provide('key', value) 提供数据,任意深度的后代 inject('key') 直接取用,中间层完全无感。
典型场景:主题(当前亮色/暗色)、登录用户、全局配置、多层级表单(form 组件 provide 校验规则,深层 input 组件 inject 注册自己)。provide 的值可以是 ref——注入方拿到的是同一个响应式引用,祖先改值,所有后代自动更新。
三条使用纪律。第一,key 用 Symbol 或带命名空间的字符串('app:user'),避免不同库撞 key。第二,inject 给默认值:inject('theme', ref('light')),没祖先提供时不至于拿到 undefined 直接崩。第三,写权限留在提供方:惯例是 provide 的同时提供一个修改方法(provide('user', { user, setUser })),注入方只读或调用方法改,避免「谁都能改、出了问题找不到凶手」。
provide/inject 是组件树内的依赖注入,不是全局状态管理——页面级共享、跨页面持久化请用下一章的 Pinia。它最适合「以某个组件为根的一片子树」内的共享。
示例
用 Map 模拟组件树上的 provide/inject 查找链(就近原则),并验证响应式共享(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// 组件树:每个节点可向父链查找注入
function createNode(name, parent = null) {
const provided = new Map();
return {
name,
provide(key, value) {
provided.set(key, value);
},
inject(key, fallback) {
let node = this;
while (node) {
if (node._provided.has(key)) return node._provided.get(key);
node = node._parent;
}
return fallback; // 没找到用默认值
},
_provided: provided,
_parent: parent,
};
}
// 树:App → Layout → Sidebar → MenuItem
const app = createNode('App');
const layout = createNode('Layout', app);
const sidebar = createNode('Sidebar', layout);
const menuItem = createNode('MenuItem', sidebar);
// App 提供主题(响应式引用),Layout 提供局部配置
const theme = { value: 'light' };
app.provide('app:theme', theme);
layout.provide('layout:cols', 3);
// 深层组件直接 inject,中间层零感知
const injectedTheme = menuItem.inject('app:theme', null);
const cols = menuItem.inject('layout:cols', 1);
const missing = menuItem.inject('app:user', '未登录');
assert.strictEqual(injectedTheme, theme);
assert.strictEqual(cols, 3);
assert.strictEqual(missing, '未登录'); // 默认值兜底
// 祖先改值,后代自动看到(同一个响应式引用)
theme.value = 'dark';
assert.strictEqual(menuItem.inject('app:theme', null).value, 'dark');
console.log('MenuItem 注入: 主题 =', injectedTheme.value, ', 列数 =', cols, ', 用户 =', missing);
console.log('主题切换后:', menuItem.inject('app:theme', null).value, '(无需逐层传 props)');
真实代码:
<script setup>
// App.vue —— 提供方
import { ref, provide } from 'vue';
const theme = ref('light');
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light';
}
provide('app:theme', { theme, toggleTheme }); // 连修改方法一起给
</script>
<template>
<Layout />
</template>
<script setup>
// MenuItem.vue —— 任意深度的注入方
import { inject } from 'vue';
const { theme, toggleTheme } = inject('app:theme');
</script>
<template>
<button :class="theme" @click="toggleTheme">当前:{{ theme }}</button>
</template>
常见坑
- inject 不给默认值:祖先没 provide 时拿到 undefined,模板里一访问属性就白屏报错。
- key 撞车:第三方库和自己都用 'theme' 这种通用 key,注入到错的数据;用 Symbol 或命名空间字符串。
- 注入方直接改值:破坏单向数据流,问题难以追踪;写操作通过提供的方法走。
- 在 v-for 子组件里 provide:每个实例都 provide 同名 key 时,注入方拿到的是最近祖先的——通常不是你想要的全局单例。
- 拿 provide/inject 替代 Pinia:跨页面、需持久化、需 devtools 追踪的状态请用 Pinia,注入适合子树内共享。
小结
provide 在祖先、inject 在任意后代,专治 prop drilling;给默认值、管好写权限、key 防撞车。下一章把多个页面组织起来:Vue Router。