前言
日常开发时有些特殊的场景需要在非 setup
期间调用inject
函数,比如app中使用provide
注入的配置信息需要在发送http
请求时带上传给后端。对此我们希望不在每个发起请求的地方去修改,而是在发起请求前的拦截进行统一处理,对此我们就需要在拦截请求的函数中使用inject
拿到app
注入的配置信息。
为什么只能在setup
期间调用inject
函数
inject
的用法大家应该都清楚,是一个用于注入依赖的函数,它可以将父组件或根组件 app 中通过 provide 提供的相同 key 的值注入到当前组件中。
我们先来看看简化后的provider
和inject
的源码,其实非常简单。
provider
函数源码
我们先来看看简化后的provider
函数源码,其实很简单:
export function provide(
key,
value,
) {
//拿到当前组件的vue实例提供的provides对象
let provides = currentInstance.provides
//拿到父组件的vue实例提供的provides对象
const parentProvides =
currentInstance.parent && currentInstance.parent.provides
// 如果父组件和当前组件的provides对象相等
if (parentProvides === provides) {
// 基于父组件的provides对象拷贝出一个新的对象
provides = currentInstance.provides = Object.create(parentProvides)
}
// 如果provides对象中有相同的key,那么就会直接覆盖。
provides[key] = value
}
在初始化一个vue
实例的时候会将父组件的provides
对象赋值给当前实例的provides
对象,所以当第一次provide
方法被调用后,会判断当前的provides
对象是否等于父组件provides
对象,如果相等就会基于父组件实例的provides
对象拷贝一个新的provides
对象。
此时父组件和子组件的provides
对象经过Object.create(parentProvides)
后就已经不是同一个对象了。如果子组件和父组件provide
对象中都有相同的key
,经过provides[key] = value
后就会将原本父组件赋值的相同key
的值“覆盖”掉。因为父组件的provides
对象是从他的父组件provides
对象拷贝的而来,所以子组件包含了父组件链上的所有的provide
提供的值。
机智如你现在应该能够理解为什么官网会说“父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值”。
inject
函数源码
现在我们再来看看简化后的inject
函数源码,同样也非常简单:
export function inject(
key,
) {
//currentInstance是一个存储当前vue实例的全局变量,在vue组件初始化时会赋值。
//初始化完成后会被重置为null
const instance = currentInstance
if (instance || currentApp) {
// 拿到父组件或者currentApp中提供的provides对象
const provides = instance
? instance.parent.provides
: currentApp!._context.provides
// 从provides对象中拿到相同key的值
if (provides && key in provides) {
return provides[key]
}
} else if (__DEV__) {
// 不是在setup中或者runWithContext中调用,就会发出警告
warn(`inject() can only be used inside setup() or functional components.`)
}
}
我们首先来看看currentInstance
这个全局变量,setup
只会在初始化vue实例的时候执行一次,在setup
期间currentInstance
会被赋值为当前组件的vue实例。等vue实例初始化完成后currentInstance
就会被赋值为null
。
前面我们已经介绍了组件的provides
对象中是包含了父组件链上的所有provides
的key,所以我们这里只需要从当前vue
实例instance
的parent
中的provides
对象中就可以取出注入相同key
的值。
看到这里相信你已经知道了为什么只能在setup
期间调用调用inject
方法了。因为只有在setup
期间currentInstance
全局变量的值为当前组件的vue
实例对象,当vue
实例初始化完成后currentInstance
已经被赋值为null。所以当我们在非setup
期间调用inject
方法会警告:inject() can only be used inside setup() or functional components.
至于currentApp
其实是另外一个全局变量,在调用app.runWithContext
方法时会给它赋值,这个下一节我们讲app.runWithContext
的时候会详细讲。
使用app.runWithContext()
打破inject
只能在setup
期间调用的限制
app.runWithContext()
的官方解释为“使用当前应用作为注入上下文执行回调函数”。这个解释乍一看很容易一脸懵逼,不着急我慢慢给你解释。
我们先来看看runWithContext
方法接收的参数和返回的值。这个方法接收一个参数,参数是一个回调函数。这个回调函数会在app.runWithContext()
执行时被立即执行,并且app.runWithContext()
的返回值就是回调函数的返回值。
我们再来看一个使用runWithContext
的例子,这行代码是拦截请求时才执行。作用是拿到app
中注入的userType
字段,注意不是在setup
期间执行。
const userType = app.runWithContext(() => {
// 拿到app中注入的userType字段
return inject("userType");
});
按照我们前一节的分析,inject
需要在setup
中执行才能拿到当前的vue
实例。但是之前还有一个currentApp
变量我们没有解释,再来回顾一下上一节的inject
源码。如果我们拿不到当前的vue
实例,就会去看一下全局变量currentApp是否存在,如果存在那么就从currentApp
中去拿provides
对象。这个currentApp
就是官方解释的“注入的上下文”,所以我们才可以在非setup
期间执行inject
,并且还可以拿到注入的值。
if (instance || currentApp) {
// 拿到父组件或者currentApp中提供的provides对象
const provides = instance
? instance.parent.provides
: currentApp!._context.provides
// 从provides对象中拿到相同key的值
if (provides && key in provides) {
return provides[key]
}
}
我们再来看看runWithContext
的源码,其实非常简单。
runWithContext(fn) {
// 将调用runWithContext方法的对象赋值给全局对象currentApp
currentApp = app
try {
// 立即执行传入的回调函数
return fn()
} finally {
currentApp = null
}
}
这里的app
就是调用runWithContext
方法的对象,你可以简单的理解为this
。调用app.runWithContext()
就会将app
对象赋值给全局变量currentApp
,然后会立即执行传入的回调fn
。当执行到回调中的inject("userType")
时,由于我们在上一行代码已经给全局变量currentApp
赋值为app
了,所以就可以从app中拿到对应key的provider
值。
总结
这篇文章我们先介绍了由于inject
执行期间需要拿到当前的vue实例,然后才能从父组件提供的provides
对象中找到相同key的值。如果我们在非 setup
期间执行,那么就拿不到当前vue实例。也找不到父组件,当然inject
也没法拿到注入的值。
在一些场景中我们确实需要在非 setup
期间执行inject
,这时我们就可以使用app.runWithContext()
将app
对象作为注入上下文执行回调函数。然后在inject
执行期间就能从app
中拿到提供的provides
对象中相同key
的值。
如果我的文章对你有点帮助,欢迎关注公众号:【欧阳码农】,文章在公众号首发。你的支持就是我创作的最大动力,感谢感谢!