Vue 依赖注入实现分析 (inject.js)

189 阅读3分钟

Vue 依赖注入实现分析 (inject.js)

文件概述

inject.js 是 Vue 实现依赖注入机制的核心文件,负责 provide/inject API 的底层实现。该文件定义了三个核心函数:

  • initProvide:初始化提供的数据
  • initInjections:初始化注入的数据
  • resolveInject:解析注入数据来源

核心函数分析

1. initProvide - 初始化提供数据

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

功能解析

  • 初始化组件实例的 provide 数据
  • 支持两种提供方式:
    • 对象形式:直接使用提供的对象
    • 函数形式:调用函数获取提供的对象,允许访问组件实例
  • 将处理后的数据存储在 vm._provided 中,作为子组件注入的数据源

2. initInjections - 初始化注入数据

export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
          )
        })
      } else {
        defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
  }
}

功能解析

  • 首先调用 resolveInject 解析注入数据
  • 暂时关闭观察者系统(toggleObserving(false)
  • 遍历所有解析到的注入属性:
    • 开发环境:将属性定义为响应式,并添加自定义 setter 警告,防止直接修改注入的值
    • 生产环境:简单地将属性定义为响应式
  • 最后恢复观察者系统(toggleObserving(true)

注意点

  • 使用 defineReactive 使注入的属性成为响应式数据
  • 开发环境特别添加了修改警告,因为修改注入的值是反模式
  • 通过 toggleObserving 暂时禁用深度观察,优化性能

3. resolveInject - 解析注入数据

export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    const result = Object.create(null)
    const keys = hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject)

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      // #6574 in case the inject object is observed...
      if (key === '__ob__') continue
      const provideKey = inject[key].from
      let source = vm
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      if (!source) {
        if ('default' in inject[key]) {
          const provideDefault = inject[key].default
          result[key] = typeof provideDefault === 'function'
            ? provideDefault.call(vm)
            : provideDefault
        } else if (process.env.NODE_ENV !== 'production') {
          warn(`Injection "${key}" not found`, vm)
        }
      }
    }
    return result
  }
}

功能解析

  • 创建一个无原型对象 result 存储解析结果
  • 处理 inject 配置的键,支持 Symbol 类型
  • 跳过 __ob__ 属性(Vue 观察者标记)
  • 对每个注入声明:
    1. 获取要查找的提供键值 provideKey = inject[key].from
    2. 沿组件树向上查找:
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      
    3. 处理未找到情况:
      • 如果有默认值,使用默认值(支持函数形式)
      • 开发环境下,没有默认值则发出警告

核心设计

  • 利用组件层级 $parent 向上遍历查找数据
  • 支持别名机制:from 属性定义真实键名
  • 支持默认值:未找到时使用 default
  • 优雅降级:未找到且无默认值时,只警告不报错

依赖注入实现原理

Vue 的依赖注入系统基于以下关键设计:

  1. 单向数据流:数据从祖先组件流向后代组件
  2. 隐式依赖:后代组件不需要知道哪个祖先提供了数据
  3. 组件树遍历:利用 $parent 链向上查找,直到找到提供数据的组件

整个流程如下:

  1. 祖先组件通过 provide 选项提供数据,存储在 _provided 属性中
  2. 后代组件通过 inject 选项声明需要的数据
  3. Vue 在组件实例化过程中:
    • 调用 initInjections 初始化注入数据
    • resolveInject 沿组件树向上查找,直到找到匹配的提供数据
    • 将找到的数据定义为组件实例的响应式属性

与生命周期的关系

依赖注入在组件初始化过程中的调用时机非常关键:

Vue.prototype._init = function (options) {
  // ...
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // 在 data/props 之前解析注入
  initState(vm)      // 初始化 data 和 props
  initProvide(vm)    // 在 data/props 之后解析提供
  callHook(vm, 'created')
  // ...
}

注意两个关键点:

  1. initInjectionsbeforeCreate 之后、initState 之前调用

    • 这确保注入的属性可以在 datacomputed 中使用
    • 但注入的属性不能在 beforeCreate 钩子中访问
  2. initProvideinitState 之后、created 之前调用

    • 这确保提供的数据可以访问组件的 dataprops
    • 完整的提供内容在 created 钩子中已经准备就绪

最佳实践与注意事项

  1. 避免直接修改注入值

    // 错误做法
    this.injectedValue = 'new value' // 会被提供组件的重新渲染覆盖
    
    // 正确做法
    this.localCopy = this.injectedValue // 创建本地副本修改
    
  2. 使用工厂函数提供响应式数据

    provide() {
      return {
        user: this.user // 直接提供响应式对象引用
      }
    }
    
  3. 正确使用默认值

    inject: {
      user: {
        from: 'userData', // 从祖先组件查找 userData 键
        default: () => ({ name: '默认用户' }) // 对象或数组默认值应使用工厂函数
      }
    }
    

总结

Vue 的依赖注入系统实现简洁而强大,让组件之间可以在不传递 props 的情况下共享数据,适合深层嵌套组件的场景。inject.js 文件的实现展示了 Vue 在保持 API 简洁性的同时,如何处理复杂的组件通信需求和各种边缘情况。

这种设计既保持了单向数据流的优点,又为特定场景提供了更灵活的组件通信方式,是 Vue 组件系统的重要补充。