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对象同样可以查找到原型链上的属性(也就是从父组件上继承的属性),而不用向上递归查找。这其实是原型模式的应用,之前从来没在实际开发当中用过原型链,这次学习了
相关推荐
NiNg_1_23423 分钟前
JS模块化工具requirejs详解
开发语言·javascript·ecmascript
果子切克now29 分钟前
vue2与vue3知识点
前端·javascript·vue.js
积水成江1 小时前
Vite+Vue3+SpringBoot项目如何打包部署
java·前端·vue.js·windows·spring boot·后端·nginx
一丝晨光1 小时前
Web技术简史、前后端分离、游戏
前端·javascript·css·游戏·unity·前后端分离·cocos
假客套1 小时前
2024 uniapp入门教程 01:含有vue3基础 我的第一个uniapp页面
前端·uni-app·vue3·hbuilder x
柒小毓1 小时前
网站开发基础:HTML、CSS
前端·css·html
哪 吒2 小时前
华为OD机试 - 冠亚军排名(Python/JS/C/C++ 2024 E卷 100分)
javascript·python·华为od
&白帝&3 小时前
Vue.js 过渡 & 动画
前端·javascript
计算机程序设计开发3 小时前
人口普查管理系统基于VUE+SpringBoot+Spring+SpringMVC+MyBatis开发设计与实现
vue.js·spring boot·毕业设计·课程设计·计算机毕业设计·计算机毕业论文
总是学不会.3 小时前
SpringBoot项目:前后端打包与部署(使用 Maven)
java·服务器·前端·后端·maven