你不知道的vue3:使用runWithContext实现在非 setup 期间使用inject
前言
日常开发时有些特殊的场景需要在非
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
的值。
如果我的文章对你有点帮助,欢迎关注公众号:【欧阳码农】,文章在公众号首发。你的支持就是我创作的最大动力,感谢感谢!