Vue 源码解析(八):provide & inject

Vue provide & inject 源码解析

引入

依赖注入是Vue中负责组件间共享代码的功能,在应用程序的规模增大时,由于组件树的深度会变的很深,使用props进行组件间传值往往会非常麻烦。因此Vue推出了这个功能,用来将依赖透传给所有的子组件。

尽管如此,我在实际开发中很少使用这个功能。对我个人而言,使用provideinject进行组件间的代码复用/传值存在着全局变量的缺点,我无法清晰的知道provide逻辑的发生位置和具体内容,调试和查找引用;而app.provide虽然可以提供全局级别的属性共享,但是使用app.config.globalProperties进行全局属性的注册也一样可以达到目的,并且这是一种更常用的做法。

总体来说,我并没有找到依赖注入的最佳实践,它或许有他的用处,但是应用场景实在太窄了。因此,在实际开发中,我还是更倾向于把代码封装为模块,从而进行逻辑封装和代码复用。

说了这么多,不用归不用,源码还是要看一下的,下面就对provideinject两个API的源码进行解析。

provide

provide即提供,可以通过在组件内部通过provide提供依赖,或是通过app.provide为全局提供依赖:

ts 复制代码
// main.js
import { createApp } from 'vue'
​
const app = createApp(/* ... */)
app.provide('message', 'hello')
​
// App.vue
<script setup>
import { provide } from 'vue'
​
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>

接下来,我们分别分析两者的源码。

provide 函数

provide的核心逻辑是拿到父组件的provides对象,使用该对象作为原型创建子组件的provides对象,并在其中添加依赖。

ts 复制代码
// apiInject.ts
export function provide<T, K = InjectionKey<T> | string | number>(
  key: K,
  value: K extends InjectionKey<infer V> ? V : T
) {
  if (!currentInstance) {
    if (__DEV__) {
      warn(`provide() can only be used inside setup().`)
    }
  } else {
    let provides = currentInstance.provides
    // by default an instance inherits its parent's provides object
    // but when it needs to provide values of its own, it creates its
    // own provides object using parent provides object as prototype.
    // this way in `inject` we can simply look up injections from direct
    // parent and let the prototype chain do the work.
    const parentProvides =
      currentInstance.parent && currentInstance.parent.provides
    if (parentProvides === provides) {
      provides = currentInstance.provides = Object.create(parentProvides)
    }
    // TS doesn't allow symbol as index type
    provides[key as string] = value
  }
}

由于在创建子组件时,子组件的provides默认指向为父组件的provides(代码见下),因此子组件如果要创建自己的provides,是不能直接在provides中添加依赖的,因为这样会影响到父组件。

ts 复制代码
// 省略了无关代码
export function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
  suspense: SuspenseBoundary | null
) {
  // inherit parent app context - or - if root, adopt from root vnode
  const appContext =
    (parent ? parent.appContext : vnode.appContext) || emptyAppContext
  
  // 在创建组件实例时,如果组件有父组件,则直接继承父组件的provides
  // 如果没有父组件(即组件是根组件),则会继承appContext的provides(即继承app.provides提供的全局依赖)
  const instance: ComponentInternalInstance = {
    provides: parent ? parent.provides : Object.create(appContext.provides),
  }
  return instance
}

所以源码采用了使用Object.create,用父组件的provides作为原型创建新的对象,并在新的对象中添加依赖。这样子组件的provides不仅拥有自己的依赖,而且还可以通过原型链的机制查找到父组件提供的依赖。

这里使用原型链最大的好处是,子组件只用维护自己的依赖,而父组件及以上层级提供的依赖只需要借助原型链机制进行查找即可。如果不使用原型链,而是让子组件记录父组件提供的依赖,那么当父组件提供了新的依赖时,还需要遍历修改子组件,这无疑是非常麻烦的。

此外,由于原型链上的属性存在覆盖关系,因此如果子组件和父组件都提供了某个同名的值,子组件的值会覆盖父组件,通过原型链这一点可以非常自然地实现。

app.provide

app.provide方法在createApp方法中,创建app对象时提供,这里提供的依赖在上面提到的createComponentInstance函数中会提供给根组件,从而可以供所有组件注入,这部分具体内容可以参考我之前写的createApp解析文章。

ts 复制代码
const app: App = (context.app = {
  use(plugin: Plugin, ...options: any[]) {
    if (installedPlugins.has(plugin)) {
      __DEV__ && warn(`Plugin has already been applied to target app.`)
    } else if (plugin && isFunction(plugin.install)) {
      installedPlugins.add(plugin)
      plugin.install(app, ...options)
    } else if (isFunction(plugin)) {
      installedPlugins.add(plugin)
      plugin(app, ...options)
    } else if (__DEV__) {
      warn(
        `A plugin must either be a function or an object with an "install" ` +
        `function.`
      )
    }
    return app
  },
  // 省略其他方法和属性
})

inject

inject源码的核心逻辑也很简单,本质是拿到当前组件实例,如果当前组件实例不存在,说明是调用的app.runWithContext(),那么就直接去找全局的provides;如果当前组件实例存在,则继续判断父组件是否存在:

  • 父组件存在:直接拿到父组件的provides,由于有上文提到的原型链,因此父组件的provides中实际上存储了一整条组件通路上的所有提供的依赖
  • 父组件不存在:说明是根组件,则去找全局的依赖,即appContext.provides

关于默认值和工厂函数的逻辑也非常好懂,这里就不赘述了。

ts 复制代码
export function inject(
  key: InjectionKey<any> | string,
  defaultValue?: unknown,
  treatDefaultAsFactory = false
) {
  // fallback to `currentRenderingInstance` so that this can be called in
  // a functional component
  const instance = currentInstance || currentRenderingInstance
​
  // also support looking up from app-level provides w/ `app.runWithContext()`
  if (instance || currentApp) {
    // #2400
    // to support `app.use` plugins,
    // fallback to appContext's `provides` if the instance is at root
    const provides = instance
      ? instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides
      : currentApp!._context.provides
​
    if (provides && (key as string | symbol) in provides) {
      // TS doesn't allow symbol as index type
      return provides[key as string]
    } else if (arguments.length > 1) {
      return treatDefaultAsFactory && isFunction(defaultValue)
        ? defaultValue.call(instance && instance.proxy)
        : defaultValue
    } else if (__DEV__) {
      warn(`injection "${String(key)}" not found.`)
    }
  } else if (__DEV__) {
    warn(`inject() can only be used inside setup() or functional components.`)
  }
}

思考与总结

  • 使用原型链简化查找:可以使用原型链进行对象属性的继承。源码通过使用原型链搭建了provides的链条,使得每个组件只需要维护自己的provides对象,访问子组件的provides对象同样可以查找到原型链上的属性(也就是从父组件上继承的属性),而不用向上递归查找。这其实是原型模式的应用,之前从来没在实际开发当中用过原型链,这次学习了
相关推荐
京东云开发者几秒前
GPT-5.6、ChatGPT Work 与 Codex 更新食用指南
前端
Cao_Ron12 分钟前
用 ECharts graph 画出卡片式矩形节点
前端
skiyee20 分钟前
换个底座的 Wot Starter 居然让 AI 更懂项目!
前端·ai编程
anyup26 分钟前
这一套绝了!AI 大模型写故事,星云 SDK 驱动 3D 数字人实时演绎
前端·人工智能·aigc
耀耀切克闹灬1 小时前
短链跳转
前端
孟陬1 小时前
高质量全方位的 AI 工具 sse-stuntman — Mock SSE 接口
前端·node.js·openai
Cao_Ron1 小时前
给 ECharts 6 graph 节点叠加角标:从原理到性能优化
前端
WebGirl1 小时前
内网穿透方式
前端
hoLzwEge2 小时前
前端项目的基石:深入解读 package.json 的作用与核心配置
前端·前端框架
奇奇怪怪的2 小时前
Agentic RAG:Agent 驱动的自主检索
前端