关于前端:inject-不生效依赖注入背后的实现原理和运行逻辑是怎样的

1次阅读

共计 5462 个字符,预计需要花费 14 分钟才能阅读完成。


一个问题

如上图所示,咱们先来思考一个问题,宿主我的项目应用了业务组件库中的组件,而后在宿主我的项目中向业务组件注入了一个名为 datekey,其值为以后的工夫戳,问 业务组件能够拿到宿主我的项目注入的数据吗?

在答复这个问题之前,咱们先来看一下 provide 和 inject 的应用形式。

依赖注入

provide

要为组件后辈供应数据,须要应用到 provide() 函数:

<script setup>
import {provide} from 'vue'

provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>

如果不应用 <script setup>,请确保 provide() 是在 setup() 同步调用的:

import {provide} from 'vue'

export default {setup() {provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
  }
}

provide() 函数接管两个参数。第一个参数被称为 注入名 ,能够是一个字符串、数值或 Symbol。后辈组件会用注入名来查找冀望注入的值。一个组件能够屡次调用 provide(),应用不同的注入名,注入不同的依赖值。

第二个参数是供应的值,值能够是任意类型,包含响应式的状态,比方一个 ref:

import {ref, provide} from 'vue'

const count = ref(0)
provide('key', count)

供应的响应式状态使后辈组件能够由此和供给者建设响应式的分割。

应用层 provide

除了供应一个组件的数据,咱们还能够在整个利用层面做供应:

import {createApp} from 'vue'

const app = createApp({})

app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')

利用级的供应在利用的所有组件中都能够注入。这在你编写插件时会特地有用,因为插件个别都不会应用组件模式来供应值。

inject

要注入先人组件供应的数据,需应用 inject() 函数:

<script setup>
import {inject} from 'vue'

const message = inject('message')
</script>

如果供应的值是一个 ref,注入进来的就是它自身,而 不会 主动解包。这使得被注入的组件放弃了和供给者的响应性连贯。

同样的,如果没有应用 <script setup>inject() 须要在 setup() 同步调用:

import {inject} from 'vue'

export default {setup() {const message = inject('message')
    return {message}
  }
}

注入的默认值

默认状况下,inject 假如传入的注入名会被某个先人链上的组件提供。如果该注入名确实没有任何组件提供,则会抛出一个运行时正告。

如果在供应的一侧看来属性是可选提供的,那么注入时咱们应该申明一个默认值,和 props 相似:

// 如果没有先人组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')

在一些场景中,默认值可能须要通过调用一个函数或初始化一个类来获得。为了 防止在不应用可选值的状况下进行不必要的计算或产生副作用,咱们能够应用工厂函数来创立默认值

const value = inject('key', () => new ExpensiveClass())

配合响应性

当应用响应式 provide/inject 值时, 倡议尽可能将任何对响应式状态的变更都放弃在 provider 外部 。这样能够确保 provide 的状态和变更操作都在同一个组件内,使其更容易保护。

有的时候,咱们可能须要在 injector 组件中更改数据。在这种状况下,咱们举荐在 provider 组件内提供一个更改数据办法:

<!-- 在 provider 组件内 -->
<script setup>
import {provide, ref} from 'vue'

const location = ref('North Pole')

function updateLocation() {location.value = 'South Pole'}

provide('location', {
  location,
  updateLocation
})
</script>
<!-- 在 injector 组件 -->
<script setup>
import {inject} from 'vue'

const {location, updateLocation} = inject('location')
</script>

<template>
  <button @click="updateLocation">{{location}}</button>
</template>

最初,如果你想确保从 provide 传过来的数据不能被 injector 的组件更改,你能够应用 readonly() 来包装提供的值。

<script setup>
import {ref, provide, readonly} from 'vue'

const count = ref(0)
provide('read-only-count', readonly(count))
</script>

应用 Symbol 作为注入名

至此,咱们曾经理解了如何应用字符串作为注入名。但如果你正在构建大型的应用程序,蕴含十分多的依赖供应,或者你正在编写提供给其余开发者应用的组件库,倡议最好应用 Symbol 来作为注入名以防止潜在的抵触。

倡议在一个独自的文件中导出这些注入名 Symbol:

export const myInjectionKey = Symbol()
// 在供给方组件中
import {provide} from 'vue'
import {myInjectionKey} from './keys.js'

provide(myInjectionKey, { /*
  要供应的数据
*/ });
// 注入方组件
import {inject} from 'vue'
import {myInjectionKey} from './keys.js'

const injected = inject(myInjectionKey)

实现原理

在对依赖注入有一个大抵的理解之后咱们来看一下其实现的原理是怎么的。间接上源码:

export function provide<T>(key: InjectionKey<T> | string | number, value: T) {if (!currentInstance) {if (__DEV__) {warn(`provide() can only be used inside setup().`)
    }
  } else {
    let provides = currentInstance.provides
    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
  }
}
export function inject<T>(key: InjectionKey<T> | string): T | undefined
export function inject<T>(
  key: InjectionKey<T> | string,
  defaultValue: T,
  treatDefaultAsFactory?: false
): T
export function inject<T>(
  key: InjectionKey<T> | string,
  defaultValue: T | (() => T),
  treatDefaultAsFactory: true
): T

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
  if (instance) {
    // #2400
    // to support `app.use` plugins,
    // fallback to appContext's `provides` if the instance is at root
    const provides =
      instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.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.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.`)
  }
}

源码地位:packages/runtime-core/src/apiInject.ts

先不论结尾提出的问题,咱们先来看一下 provide 的源码,留神上面这句代码:

if (parentProvides === provides) {provides = currentInstance.provides = Object.create(parentProvides);
}

这里要解决一个问题,当父级 key 和 爷爷级别的 key 反复的时候,对于子组件来讲,须要取最近的父级别组件的值,那这里的解决方案就是利用原型链来解决。

provides 初始化的时候是在 createComponent 时解决的,过后是间接把 parent.provides 赋值给组件的 provides,所以,如果说这里发现 provides 和 parentProvides 相等的话,那么就阐明是第一次做 provide(对于以后组件来讲),咱们就能够把 parent.provides 作为 currentInstance.provides 的原型从新赋值。

至于为什么不在 createComponent 的时候做这个解决,可能的益处是在这里初始化的话,是有个懒执行的成果(优化点, 只有须要的时候才初始化 )。

看完了 provide 的源码,咱们再来看一下 inject 的源码。

inject 的执行逻辑比较简单,首先拿到以后实例,如果以后实例存在的话进一步判断以后实例的父实例是否存在,如果父实例存在则取父实例的 provides 进行注入,如果父实例不存在则取全局的(appContext)的 provides 进行注入。

inject 生效?

在看完 provide 和 inject 的源码之后,咱们来剖析一下文章结尾提出的问题。

咱们在业务组件中注入了来自宿主我的项目的 provide 进去的 key,业务组件首先会去寻找以后组件(instance),而后依据以后组件寻找父组件的 provides 进行注入即可,显然咱们在业务组件中是能够拿到宿主我的项目注入进来的数据的。

第二个问题

剖析完了文章结尾提出的问题,咱们再来看一个有意思的问题。下图中的业务组件能拿到宿主我的项目注入的数据吗?

答案可能跟你想的有点不一样: 这个时候咱们就拿不到宿主我的项目注入的数据了!!!

问题出在了哪里?

问题出在了 Symbol 这里,事实上在这个场景下,宿主我的项目引入的 Symbol 和 业务组件库引入的 Symbol 实质上 并不是同一个 Symbol,因为在 不同利用中创立的 Symbol 实例总是惟一的

如果想要所有的利用共享一个 Symbol 实例,这个时候咱们就须要另一个 API 来创立或获取 Symbol,那就是 Symbol.for(),它能够注册或获取一个 window 全局的 Symbol 实例。

咱们的公共二方库(common)只须要做如下批改即可:

export const date = Symbol.for('date');

总结

咱们要想 inject 下层提供的 provide 须要留神以下几点:

  • 确保 inject 和 provide 的组件在同一颗组件树中
  • 若应用 Symbol 作为 key 值,请确保两个组件处于同一个利用中
  • 若两个组件不处于同一个利用中,请应用 Symbol.for 创立全局的 Symbol 实例作为 key 值应用

参考

更多精彩请关注咱们的公众号「百瓶技术」,有不定期福利呦!

正文完
 0