Skip to content

实现 provide/inject

本节对标 Vue 3 源码 @vue/runtime-core 中的 apiInject.ts

provide/inject 是什么?

provideinject 是 Vue 3 中实现跨层级组件通信的 API。祖先组件通过 provide 提供数据,后代组件通过 inject 获取数据,无需逐层传递 props。

GrandParent (provide 'theme')

    └── Parent(不需要知道 theme 的存在)

            └── Child (inject 'theme')  ← 直接获取

这解决了 props drilling 问题 —— 中间层组件不需要为了透传数据而添加不相关的 props。

provide 的实现

对标 packages/runtime-core/src/apiInject.ts —— provide

ts
// provide 函数:在当前组件实例上注册一个可供后代组件 inject 的值
export function provide<T>(key: string | symbol, value: T) {
  // 获取当前正在执行 setup 的组件实例(只能在 setup 中调用 provide)
  const currentInstance = getCurrentInstance()

  if (currentInstance) {
    let { provides } = currentInstance
    // 获取父组件的 provides 用于比较
    const parentProvides = currentInstance.parent?.provides

    // 关键逻辑:惰性创建 provides 对象
    // 初始状态下 instance.provides === parent.provides(指向同一个对象)
    // 当组件首次调用 provide 时,才创建一个以父组件 provides 为原型的新对象
    // 这样既避免了修改父组件的 provides,又能通过原型链继承所有祖先的 provides
    if (provides === parentProvides) {
      provides = currentInstance.provides = Object.create(
        parentProvides || null,
      )
    }

    // 将 key-value 存储到当前组件自己的 provides 对象上
    provides[key] = value
  }
}

原型链继承机制

这是 provide/inject 最精妙的设计。让我们一步步分析:

初始状态:

createComponentInstance 中,组件的 provides 直接指向父组件的 provides:

ts
// 创建组件实例时,provides 直接引用父组件的 provides 对象
// 如果组件不调用 provide,它的 provides 和父组件共享同一个对象(零开销)
function createComponentInstance(vnode, parent) {
  const instance = {
    // ...
    provides: parent ? parent.provides : {},  // 继承父组件的 provides 引用
  }
  return instance
}

首次 provide 时:

ts
if (provides === parentProvides) {
  provides = currentInstance.provides = Object.create(parentProvides || null)
}
provides[key] = value

使用 Object.create(parentProvides) 创建一个以父组件 provides 为原型的新对象。这样:

  1. 当前组件的 provide 值存在自身属性上
  2. 父组件(及更上层)的 provide 值可以通过原型链访问
  3. 当前组件的 provide 不会影响父组件的 provides 对象

图解原型链

GrandParent.provides = { theme: 'dark', lang: 'zh' }

    │  Object.create(GrandParent.provides)

Parent.provides = { fontSize: 14 }
    │  [[Prototype]] → GrandParent.provides

    │  Object.create(Parent.provides)

Child.provides = { color: 'red' }
    │  [[Prototype]] → Parent.provides
    │                      │  [[Prototype]] → GrandParent.provides

在 Child 中:

  • Child.provides.color'red'(自身属性)
  • Child.provides.fontSize14(原型链 → Parent)
  • Child.provides.theme'dark'(原型链 → GrandParent)

为什么不直接复制?

ts
// ❌ 错误方式:浅拷贝
provides = currentInstance.provides = { ...parentProvides }
provides[key] = value

浅拷贝有两个问题:

  1. 性能差 — 每个组件都要复制整个 provides 对象
  2. 无法感知上层变化 — 如果祖先组件后续又 provide 了新值,拷贝的对象不会更新

用原型链:

  1. 零拷贝 — 只在有 provide 调用时才创建新对象
  2. 自动继承 — 通过原型链自然获取所有祖先的 provides

没有 provide 的组件

如果一个组件没有调用 provide,它的 provides 就是父组件的 provides(初始化时赋值的引用)。因此不会创建多余的对象:

ts
// createComponentInstance 中
provides: parent ? parent.provides : {}

// 没有调用 provide → provides 始终是 parent.provides 的引用
// 不会触发 Object.create

inject 的实现

对标 packages/runtime-core/src/apiInject.ts —— inject

ts
// inject 函数:从祖先组件的 provides 中获取指定 key 的值
export function inject<T>(
  key: string | symbol,                  // 要注入的 key,需与 provide 时使用的 key 一致
  defaultValue?: T | (() => T),          // 可选的默认值,找不到时使用
  treatDefaultAsFactory = false,          // 是否将函数类型的默认值当作工厂函数执行
): T | undefined {
  // 获取当前正在执行 setup 的组件实例
  const currentInstance = getCurrentInstance()

  if (currentInstance) {
    // 从父组件的 provides 中查找(不查找自身的 provides)
    // 因为 provide/inject 的语义是"祖先提供,后代消费"
    const provides = currentInstance.parent?.provides

    // 使用 in 操作符检查 key 是否存在(会沿原型链向上查找)
    if (provides && (key as string | symbol) in provides) {
      return provides[key as string]  // 找到则返回对应的值
    } else if (arguments.length > 1) {
      // 未找到但提供了默认值
      // 如果 treatDefaultAsFactory 为 true 且默认值是函数,则调用工厂函数获取默认值
      // 否则直接返回默认值
      return treatDefaultAsFactory && typeof defaultValue === 'function'
        ? (defaultValue as Function)()
        : (defaultValue as T)
    } else {
      // 既未找到值也没有默认值,发出警告
      console.warn(`injection "${String(key)}" not found.`)
    }
  }
}

inject 查找的是 parent.provides

注意 inject 查找的是 currentInstance.parent.provides,而非 currentInstance.provides

ts
const provides = currentInstance.parent?.provides

这是因为组件不应该 inject 自己 provide 的值。provide/inject 的语义是"祖先提供,后代消费"。

原型链查找过程

ts
// Child inject('theme')
const provides = Child.parent.provides  // → Parent.provides

// Parent.provides 本身没有 theme
// 沿原型链查找:Parent.provides.[[Prototype]] = GrandParent.provides
// GrandParent.provides.theme = 'dark'
// 找到!返回 'dark'

JavaScript 的原型链机制天然实现了"沿祖先链向上查找"的逻辑,无需手动遍历组件树。

默认值支持

inject 支持三种默认值形式:

ts
// 1. 无默认值 —— 找不到时警告
const theme = inject('theme')

// 2. 静态默认值
const theme = inject('theme', 'light')

// 3. 工厂函数默认值(避免不必要的对象创建)
const config = inject('config', () => ({ debug: false }), true)

工厂函数默认值

第三个参数 treatDefaultAsFactory 控制是否将函数类型的默认值当作工厂函数执行:

ts
// 不作为工厂函数 → 直接返回函数本身
const handler = inject('handler', () => console.log('default'))
// handler 是 () => console.log('default')

// 作为工厂函数 → 执行并返回结果
const config = inject('config', () => ({ debug: false }), true)
// config 是 { debug: false }

跨层级通信示例

主题系统

ts
// 根组件:通过 provide 提供主题数据和切换方法
const App = {
  setup() {
    const theme = ref('dark')  // 响应式的主题状态
    // 提供主题值,后代组件通过 inject('theme') 获取
    provide('theme', theme)
    // 提供切换主题的方法,后代组件可以调用来修改主题
    provide('toggleTheme', () => {
      theme.value = theme.value === 'dark' ? 'light' : 'dark'
    })
    return () => h(Page)
  },
}

// 中间组件:完全不需要知道 theme 的存在,避免了 props drilling
const Page = {
  setup() {
    return () => h('div', {}, [h(Header), h(Content)])
  },
}

// 深层子组件:直接通过 inject 获取祖先组件提供的主题数据
const Header = {
  setup() {
    const theme = inject('theme')              // 注入响应式的 theme ref
    const toggleTheme = inject('toggleTheme')  // 注入切换方法

    return () =>
      h('header', { class: theme.value }, [
        // 点击按钮调用 toggleTheme 切换主题,theme.value 变化会自动触发重渲染
        h('button', { onClick: toggleTheme }, 'Toggle Theme'),
      ])
  },
}

// 另一个深层子组件:同样通过 inject 获取主题数据
const Content = {
  setup() {
    const theme = inject('theme')  // 注入同一个 theme ref

    return () =>
      // theme.value 变化时,这个组件也会自动更新
      h('main', { class: `content-${theme.value}` }, 'Content Area')
  },
}

全局状态管理

provide/inject 可以构建简单的状态管理方案:

ts
// 利用 provide/inject + reactive 构建简单的状态管理方案
function createStore() {
  // 使用 reactive 创建响应式状态对象
  const state = reactive({
    count: 0,
    user: null,
  })

  // 定义操作状态的方法(类似 Vuex 的 mutations/actions)
  const actions = {
    increment: () => state.count++,
    setUser: (user: any) => (state.user = user),
  }

  return { state, actions }
}

// 根组件:创建 store 并通过 provide 注入
const App = {
  setup() {
    const store = createStore()
    provide('store', store)  // 整棵组件树的后代都可以访问 store
    return () => h(Child)
  },
}

// 任意后代组件:通过 inject 获取 store,使用响应式状态和操作方法
const Child = {
  setup() {
    // 注入 store,获取 state(reactive 对象)和 actions
    const { state, actions } = inject('store')!

    return () =>
      h('div', {}, [
        h('span', {}, String(state.count)),                  // state.count 变化时自动更新
        h('button', { onClick: actions.increment }, '+1'),   // 点击按钮调用 increment
      ])
  },
}

使用 Symbol 作为 key

推荐使用 Symbol 作为 inject key,避免命名冲突:

ts
// keys.ts
export const ThemeKey = Symbol('theme')
export const I18nKey = Symbol('i18n')

// 提供
provide(ThemeKey, themeRef)

// 注入
const theme = inject(ThemeKey)

provide/inject 与响应式

provide 的值可以是响应式的(ref / reactive),inject 获取到的也是同一个响应式引用:

ts
// 祖先组件
const count = ref(0)
provide('count', count)

// 后代组件
const count = inject('count') // 是同一个 ref
// count.value 变化时,后代组件自动更新

如果需要防止后代组件修改 provided 的值,可以用 readonly 包装:

ts
provide('count', readonly(count))

对比 React useContext

维度Vue 3 provide/injectReact useContext
提供数据provide(key, value)<Context.Provider value={...}>
消费数据inject(key, defaultValue)useContext(Context)
创建方式无需预先创建需要 createContext()
key 类型string / SymbolContext 对象
响应式天然支持(provide ref/reactive)需要 state + setState
性能只有消费了值的组件才更新Provider value 变化时所有 Consumer 更新
默认值inject 第二个参数createContext 参数
查找机制原型链(JS 原生)React 内部向上遍历 Fiber 树

React Context 的重渲染问题

React 的 Context 有一个知名的性能问题:当 Provider 的 value 变化时,所有消费该 Context 的组件都会重渲染,即使它只使用了 value 中的一部分:

tsx
// React —— value 变化会导致所有 consumer 重渲染
const ThemeContext = createContext({ theme: 'dark', lang: 'en' })

function Child() {
  const { theme } = useContext(ThemeContext)
  // 即使只用了 theme,lang 变化时 Child 也会重渲染
  return <div className={theme}>...</div>
}

Vue 的 provide/inject 不存在这个问题,因为响应式系统会精确追踪依赖:

ts
// Vue —— 只有 theme.value 变化才会触发使用了 theme 的组件更新
const theme = inject('theme') // ref
// 只有访问 theme.value 的 render effect 才会收集依赖

测试用例

ts
describe('provide/inject', () => {
  // 测试基本的 provide/inject:父组件 provide,子组件 inject
  it('should provide and inject basic value', () => {
    let injectedValue: any

    const Parent = {
      setup() {
        provide('msg', 'hello')  // 父组件提供 'msg'
        return () => h(Child)
      },
    }

    const Child = {
      setup() {
        injectedValue = inject('msg')  // 子组件注入 'msg'
        return () => h('div', {}, injectedValue)
      },
    }

    render(h(Parent), document.createElement('div'))
    // 断言子组件成功获取到父组件 provide 的值
    expect(injectedValue).toBe('hello')
  })

  // 测试跨层级注入:孙组件可以 inject 到祖父组件 provide 的值
  it('should inject from grandparent', () => {
    let injectedValue: any

    const GrandChild = {
      setup() {
        injectedValue = inject('msg')  // 从祖父组件注入
        return () => h('div')
      },
    }

    // 中间层组件不参与 provide/inject
    const Child = {
      setup() {
        return () => h(GrandChild)
      },
    }

    const GrandParent = {
      setup() {
        provide('msg', 'from grandparent')  // 祖父组件提供数据
        return () => h(Child)
      },
    }

    render(h(GrandParent), document.createElement('div'))
    // 断言通过原型链跨层级成功注入
    expect(injectedValue).toBe('from grandparent')
  })

  // 测试默认值:当 provide 未提供指定 key 时使用默认值
  it('should use default value when not provided', () => {
    let injectedValue: any

    const Child = {
      setup() {
        // 'missing' key 不存在,使用默认值 'default'
        injectedValue = inject('missing', 'default')
        return () => h('div')
      },
    }

    const Parent = {
      setup() {
        return () => h(Child)  // 父组件没有 provide 'missing'
      },
    }

    render(h(Parent), document.createElement('div'))
    // 断言使用了静态默认值
    expect(injectedValue).toBe('default')
  })

  // 测试工厂函数默认值:第三个参数为 true 时将函数作为工厂函数执行
  it('should support factory function as default value', () => {
    let injectedValue: any

    const Child = {
      setup() {
        injectedValue = inject(
          'config',
          () => ({ debug: false }),  // 工厂函数
          true,                       // 标记为工厂函数模式
        )
        return () => h('div')
      },
    }

    const Parent = {
      setup() {
        return () => h(Child)  // 没有 provide 'config'
      },
    }

    render(h(Parent), document.createElement('div'))
    // 断言工厂函数被执行,返回了 { debug: false } 对象
    expect(injectedValue).toEqual({ debug: false })
  })

  // 测试就近覆盖:子组件 provide 的同名 key 会覆盖父组件的(对更深层组件而言)
  it('should override parent provide with child provide', () => {
    let injectedInChild: any
    let injectedInGrandChild: any

    const GrandChild = {
      setup() {
        injectedInGrandChild = inject('msg')  // 从最近的祖先查找
        return () => h('div')
      },
    }

    const Child = {
      setup() {
        injectedInChild = inject('msg')        // 这时查找的是 Parent 的 provides
        provide('msg', 'from child')           // 然后 Child 自己又 provide 了同名 key
        return () => h(GrandChild)
      },
    }

    const Parent = {
      setup() {
        provide('msg', 'from parent')          // 最外层 provide
        return () => h(Child)
      },
    }

    render(h(Parent), document.createElement('div'))
    // Child inject 到的是 Parent 提供的值
    expect(injectedInChild).toBe('from parent')
    // GrandChild inject 到的是 Child 提供的值(就近原则)
    expect(injectedInGrandChild).toBe('from child')
  })

  // 测试 provide 不影响父组件:子组件的 provide 不会污染父组件的 provides 对象
  it('should not affect parent provides when child provides', () => {
    let parentProvides: any
    let childProvides: any

    const GrandChild = {
      setup() {
        return () => h('div')
      },
    }

    const Child = {
      setup() {
        provide('childKey', 'childValue')  // 子组件提供新的 key
        childProvides = getCurrentInstance()!.provides  // 捕获子组件的 provides
        return () => h(GrandChild)
      },
    }

    const Parent = {
      setup() {
        provide('parentKey', 'parentValue')  // 父组件提供的 key
        parentProvides = getCurrentInstance()!.provides  // 捕获父组件的 provides
        return () => h(Child)
      },
    }

    render(h(Parent), document.createElement('div'))
    // 父组件的 provides 中不应该包含子组件的 key(Object.create 隔离了修改)
    expect('childKey' in parentProvides).toBe(false)
    // 子组件的 provides 通过原型链能访问到父组件的值
    expect(childProvides.parentKey).toBe('parentValue')
    // 子组件的 provides 包含自己提供的值
    expect(childProvides.childKey).toBe('childValue')
  })

  // 测试响应式 provide:provide 的 ref 值变化时,inject 的组件自动更新
  it('should work with reactive value', async () => {
    const count = ref(0)  // 响应式值

    const Child = {
      setup() {
        const c = inject<any>('count')  // 注入的是同一个 ref 对象
        return () => h('span', {}, String(c.value))  // 渲染 ref 的 value
      },
    }

    const Parent = {
      setup() {
        provide('count', count)  // provide 一个 ref
        return () => h(Child)
      },
    }

    const root = document.createElement('div')
    render(h(Parent), root)
    expect(root.innerHTML).toBe('<span>0</span>')  // 初始值

    count.value = 42   // 修改 ref 值
    await nextTick()
    // 断言子组件因 ref 值变化而自动重新渲染
    expect(root.innerHTML).toBe('<span>42</span>')
  })

  // 测试 Symbol 类型的 key:推荐方式,避免字符串 key 的命名冲突
  it('should support Symbol as injection key', () => {
    const key = Symbol('theme')  // 创建 Symbol 类型的 key
    let injectedValue: any

    const Child = {
      setup() {
        injectedValue = inject(key)  // 使用 Symbol key 注入
        return () => h('div')
      },
    }

    const Parent = {
      setup() {
        provide(key, 'dark')  // 使用同一个 Symbol key 提供
        return () => h(Child)
      },
    }

    render(h(Parent), document.createElement('div'))
    // 断言通过 Symbol key 成功完成 provide/inject
    expect(injectedValue).toBe('dark')
  })
})

本节小结

  1. provide — 将数据存储到 instance.provides,首次调用时通过 Object.create 创建原型链继承
  2. inject — 从 parent.provides 中查找值,沿原型链自动向上搜索
  3. 原型链机制 — 利用 JavaScript 原型链实现零成本的跨层级继承,无需手动遍历组件树
  4. 默认值 — 支持静态默认值和工厂函数默认值
  5. 响应式 — provide ref/reactive 值,后代组件自动追踪依赖、自动更新
  6. 跨层级通信 — 解决 props drilling 问题,适用于主题、国际化、全局状态等场景
  7. 对比 React — Vue 的 provide/inject 依赖精确追踪,不存在 React Context 的全量重渲染问题

用心学习,用代码说话 💻