one question
As shown in the figure above, let's think about a problem first. The host project uses the components in the business component library, and then injects a ---ce1125340c19d062201df1c2814d2f7c--named date
key
the business component in the host project. key
, whose value is the current timestamp, can the business component get the data injected by the host project?
Before answering this question, let's take a look at how provide and inject are used.
dependency injection
provide
To provide data for component descendants, you need to use the provide()
function:
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
If you don't use <script setup>
, make sure provide()
is called synchronously in setup()
:
import { provide } from 'vue'
export default {
setup() {
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
}
}
provide()
function accepts two parameters. The first parameter is called the injection name and can be a string, number or Symbol. Descendent components will use the injection name to look up the value that is expected to be injected. A component can be called multiple times provide()
, using different injection names and injecting different dependency values.
The second parameter is the supplied value, which can be of any type, including reactive state, such as a ref:
import { ref, provide } from 'vue'
const count = ref(0)
provide('key', count)
The reactive state of the provider allows descendant components to establish a reactive relationship with the provider from there.
Application layer provide
In addition to supplying data for a component, we can also supply it at the application level:
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
Application-level provisioning can be injected in all components of the application. This is especially useful when you're writing plugins, since plugins generally don't use components to supply values.
inject
To inject data supplied by an ancestor component, use the inject()
function:
<script setup>
import { inject } from 'vue'
const message = inject('message')
</script>
If the supplied value is a ref, it is injected into itself, not automatically unwrapped. This allows the injected component to maintain a responsive connection to the provider.
Similarly, if <script setup>
is not used, inject()
need to be called synchronously in setup()
:
import { inject } from 'vue'
export default {
setup() {
const message = inject('message')
return { message }
}
}
Injected default value
By default, inject assumes that the incoming injection name will be provided by a component on some ancestor chain. If the injection name is indeed not provided by any component, a runtime warning will be thrown.
If the property appears to be optional on the supply side, then we should declare a default value when injecting, similar to props:
// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')
In some scenarios, the default value may need to be obtained by calling a function or initializing a class. To avoid unnecessary computations or side effects without using optionals, we can use factory functions to create default values :
const value = inject('key', () => new ExpensiveClass())
Responsiveness
When using reactive provide/inject values, it is recommended to keep any changes to reactive state within the provider as much as possible . This ensures that both the state of the provide and the change operations are in the same component, making it easier to maintain.
Sometimes, we may need to change data in the injector component. In this case, we recommend providing a change data method inside the provider component:
<!-- 在 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>
Finally, if you want to ensure that the data passed from the provider cannot be changed by the injector's component, you can wrap the provided value with readonly()
.
<script setup>
import { ref, provide, readonly } from 'vue'
const count = ref(0)
provide('read-only-count', readonly(count))
</script>
Use Symbol as the injection name
So far, we have seen how to use strings as injection names. But if you are building a large application with a lot of dependencies, or you are writing a component library for other developers to use, it is recommended to use Symbol as the injection name to avoid potential conflicts.
It is recommended to export these injection name symbols in a separate file:
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)
Implementation principle
After having a general understanding of dependency injection, let's take a look at the principle of its implementation. Go directly to the source code:
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.`)
}
}
Source code location: packages/runtime-core/src/apiInject.ts
Regardless of the questions raised at the beginning, let's take a look at the source code of provide, and pay attention to the following code:
if (parentProvides === provides) {
provides = currentInstance.provides = Object.create(parentProvides);
}
There is a problem to be solved here. When the parent key and the grandfather-level key are duplicated, for the child component, the value of the nearest parent-level component needs to be taken. The solution here is to use the prototype chain to solve it.
When provides is initialized, it is processed when createComponent is created. At that time, parent.provides is directly assigned to the component's provides. Therefore, if it is found that provides and parentProvides are equal, it means that it is the first time to do provide (for the current component to ), we can reassign parent.provides as the prototype of currentInstance.provides.
As for why not do this processing when creatingComponent, the possible advantage is that if it is initialized here, it has the effect of lazy execution (optimization point, only initialize when needed ).
After reading the source code of provide, let's take a look at the source code of inject.
The execution logic of inject is relatively simple. First, get the current instance. If the current instance exists, it is further judged whether the parent instance of the current instance exists. If the parent instance exists, the parent instance's provides are used for injection. If the parent instance does not exist, the global one is used. (appContext) provides for injection.
inject fails?
After reading the source code of provide and inject, let's analyze the questions raised at the beginning of the article.
We inject the key from the provide of the host project into the business component. The business component will first look for the current component (instance), and then find the parent component's provides according to the current component and inject it. Obviously, we can do it in the business component. Get the data injected by the host project.
second question
After analyzing the questions raised at the beginning of the article, let's look at another interesting question. Can the business component in the figure below get the data injected by the host project?
The answer may be a little different from what you think: at this time, we can't get the data injected by the host project! ! !
Where is the problem?
The problem lies in the Symbol. In fact, in this scenario, the Symbol introduced by the host project and the Symbol introduced by the business component library are not essentially the same Symbol , because the Symbol instances created in different applications are always unique .
If we want all applications to share a Symbol instance, at this time we need another API to create or get Symbol, that is Symbol.for()
, it can register or get a window global Symbol instance.
Our public second-party library (common) only needs to be modified as follows:
export const date = Symbol.for('date');
Summarize
If we want to inject the provide provided by the upper layer, we need to pay attention to the following points:
- Make sure that inject and provide components are in the same component tree
- If using Symbol as the key, make sure both components are in the same application
- If the two components are not in the same application, use Symbol.for to create a global Symbol instance and use it as the key value
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。