阅读视图

发现新文章,点击刷新页面。

Vue 跨组件通信底层:provide/inject 原理与实战指南

一、provide/inject 的核心设计思想

provide/inject 是 Vue 实现依赖注入(Dependency Injection)的核心 API,其设计目标是:

  • 解决跨层级组件通信问题(props 逐级透传的痛点)
  • 实现组件间的松耦合(后代组件无需知道依赖的具体来源)
  • 维持响应式数据传递

其核心机制是:每个组件实例都维护一个 provides 对象,子组件的 provides 原型链指向父组件的 provides,形成一个原型链查找机制

二、底层实现原理的代码模拟

为了让你直观理解,我将用 JavaScript 模拟 Vue 组件实例的 provides 链和 provide/inject 方法的实现。

1. 组件实例的基础结构

// 模拟 Vue 组件实例的构造函数
class ComponentInstance {
  constructor(parent) {
    this.parent = parent; // 父组件实例引用
    this.props = {};
    this.data = {};
    
    // 核心:构建 provides 原型链
    // 如果有父组件,当前组件的 provides 继承自父组件的 provides
    // 如果没有父组件(根组件),创建一个空对象
    this.provides = parent ? Object.create(parent.provides) : Object.create(null);
  }

  // 实现 provide 方法
  provide(key, value) {
    // 将提供的键值对存储到当前组件的 provides 对象上
    this.provides[key] = value;
  }

  // 实现 inject 方法
  inject(key, defaultValue = undefined) {
    // 从当前组件的 provides 开始查找
    let provides = this.provides;
    
    // 沿着原型链向上查找(直到根组件)
    while (provides) {
      if (Object.prototype.hasOwnProperty.call(provides, key)) {
        // 找到则返回对应的值
        return provides[key];
      }
      // 找不到则继续向上查找父组件的 provides
      provides = Object.getPrototypeOf(provides);
    }
    
    // 如果最终没找到,返回默认值
    return typeof defaultValue === 'function' ? defaultValue() : defaultValue;
  }
}

2. 原型链查找机制的验证

// 创建根组件实例
const root = new ComponentInstance(null);
// 根组件提供数据
root.provide('theme', 'dark');
root.provide('user', { name: 'admin' });

// 创建子组件实例(父组件为 root)
const child = new ComponentInstance(root);
// 子组件提供自己的数据
child.provide('lang', 'zh-CN');

// 创建孙组件实例(父组件为 child)
const grandChild = new ComponentInstance(child);

// 孙组件注入数据
console.log(grandChild.inject('theme')); // 'dark' (从根组件找到)
console.log(grandChild.inject('lang'));  // 'zh-CN'(从子组件找到)
console.log(grandChild.inject('user'));  // { name: 'admin' }(从根组件找到)
console.log(grandChild.inject('age', 18)); // 18(使用默认值)
console.log(grandChild.inject('gender', () => 'male')); // 'male'(函数默认值)

3. 响应式数据的传递原理

provide/inject 本身不处理响应式,它只是传递数据引用。响应式由 Vue 的响应式系统(ref/reactive)保证:

// 模拟 Vue 的 ref 实现
class Ref {
  constructor(value) {
    this._value = value;
  }
  
  get value() {
    console.log('触发依赖收集');
    return this._value;
  }
  
  set value(newValue) {
    this._value = newValue;
    console.log('触发更新');
  }
}

// 创建响应式数据
const themeRef = new Ref('light');

// 根组件提供响应式数据
root.provide('theme', themeRef);

// 孙组件获取
const injectedTheme = grandChild.inject('theme');
console.log(injectedTheme.value); // 'light'(触发依赖收集)

// 修改值会触发响应式更新
injectedTheme.value = 'dark'; // 触发更新

三、Vue 源码中的真实实现(简化版)

下面是从 Vue 3 源码中提取的核心逻辑,展示真实的实现方式:

1. 组件实例的 provides 初始化

// 源码位置:packages/runtime-core/src/component.ts
export function createComponentInstance(vnode, parent, suspense) {
  const instance = {
    vnode,
    parent,
    provides: parent ? Object.create(parent.provides) : Object.create(appContext.provides),
    // ...其他属性
  };
  return instance;
}

2. provide 函数的实现

// 源码位置:packages/runtime-core/src/apiInject.ts
export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
  if (!currentInstance) {
    if (__DEV__) {
      warn(`provide() can only be used inside setup().`);
    }
    return;
  }
  // 获取当前组件的 provides 对象
  const provides = currentInstance.provides;
  // 获取父组件的 provides 对象(原型)
  const parentProvides =
    currentInstance.parent && currentInstance.parent.provides;

  // 如果是首次提供该 key,或者 key 的值发生变化
  if (parentProvides === provides) {
    // 继承父组件的 provides 并创建新对象
    provides = currentInstance.provides = Object.create(parentProvides);
  }
  // 将值存入 provides
  provides[key as string] = value;
}

3. inject 函数的实现

// 源码位置:packages/runtime-core/src/apiInject.ts
export function inject<T>(
  key: InjectionKey<T> | string | number,
  defaultValue?: T | (() => T),
  treatDefaultAsFactory = false
): T | undefined {
  // 获取当前组件实例
  const instance = currentInstance || currentRenderingInstance;
  
  if (instance) {
    // 优先从组件自身的 provides 查找,否则从 appContext 查找
    const provides = instance.provides || instance.appContext.provides;
    
    if (provides && (key as string | symbol) in provides) {
      // 找到则返回值
      return provides[key as string];
    } 
    // 处理默认值
    else if (arguments.length > 1) {
      return treatDefaultAsFactory && typeof defaultValue === 'function'
        ? (defaultValue as () => T)()
        : defaultValue;
    } 
    // 开发环境警告
    else if (__DEV__) {
      warn(`injection "${String(key)}" not found.`);
    }
  }
}

四、实际项目中的高级应用(结合原理)

理解原理后,我们可以更灵活地使用 provide/inject

场景:创建可复用的组件上下文

// 创建上下文的组合函数
import { provide, inject, reactive, readonly } from 'vue';

// 使用 Symbol 作为唯一键(避免命名冲突)
const TABLE_CONTEXT_KEY = Symbol('table-context');

// 父组件提供上下文
export function useTableProvider(props) {
  const tableState = reactive({
    data: props.data,
    loading: false,
    pagination: {
      page: 1,
      pageSize: 10
    },
    // 方法
    fetchData: () => {
      tableState.loading = true;
      // 实际请求逻辑...
    },
    changePage: (page) => {
      tableState.pagination.page = page;
      tableState.fetchData();
    }
  });

  // 提供只读的上下文(防止子组件修改)
  provide(TABLE_CONTEXT_KEY, readonly(tableState));
  
  return tableState;
}

// 子组件注入上下文
export function useTableInject() {
  const context = inject(TABLE_CONTEXT_KEY, () => {
    throw new Error('useTableInject must be used within a Table component');
  });
  
  return context;
}

组件中使用

<!-- Table.vue(父组件) -->
<script setup>
import { defineProps } from 'vue';
import { useTableProvider } from './useTable';

const props = defineProps({
  data: {
    type: Array,
    default: () => []
  }
});

const tableState = useTableProvider(props);
</script>

<!-- TablePagination.vue(子组件) -->
<script setup>
import { useTableInject } from './useTable';

const tableContext = useTableInject();

// 使用上下文数据
console.log(tableContext.pagination.page);
// 调用上下文方法
const handlePageChange = (page) => {
  tableContext.changePage(page);
};
</script>

总结

provide/inject 的底层原理核心要点:

  1. 原型链继承:每个组件实例的 provides 对象通过 Object.create() 继承自父组件的 provides,形成链式查找结构。
  2. 查找机制inject 时会从当前组件的 provides 开始,沿着原型链向上查找,直到找到匹配的键或到达根组件。
  3. 响应式传递provide/inject 仅传递数据引用,响应式由 Vue 的响应式系统(ref/reactive)保证。
  4. 作用域隔离:每个组件的 provides 是独立的,但通过原型链共享父组件的提供值,实现隔离与共享的平衡。
❌