fix(inject): fix edge case of provided with async-mutated getters

fix #12667
This commit is contained in:
Evan You 2022-07-16 21:33:48 +08:00
parent 25ffdb62d2
commit ea5d0f3fbf
3 changed files with 63 additions and 18 deletions

View File

@ -1,8 +1,7 @@
import { warn, hasSymbol, isFunction, isObject } from '../util/index'
import { defineReactive, toggleObserving } from '../observer/index'
import type { Component } from 'types/component'
import { provide } from 'v3/apiInject'
import { setCurrentInstance } from '../../v3/currentInstance'
import { resolveProvided } from 'v3/apiInject'
export function initProvide(vm: Component) {
const provideOption = vm.$options.provide
@ -13,12 +12,18 @@ export function initProvide(vm: Component) {
if (!isObject(provided)) {
return
}
const source = resolveProvided(vm)
// IE9 doesn't support Object.getOwnPropertyDescriptors so we have to
// iterate the keys ourselves.
const keys = hasSymbol ? Reflect.ownKeys(provided) : Object.keys(provided)
setCurrentInstance(vm)
for (let i = 0; i < keys.length; i++) {
provide(keys[i], provided[keys[i]])
const key = keys[i]
Object.defineProperty(
source,
key,
Object.getOwnPropertyDescriptor(provided, key)!
)
}
setCurrentInstance()
}
}

View File

@ -1,5 +1,6 @@
import { isFunction, warn } from 'core/util'
import { currentInstance } from './currentInstance'
import type { Component } from 'types/component'
export interface InjectionKey<T> extends Symbol {}
@ -9,19 +10,23 @@ export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
warn(`provide() can only be used inside setup().`)
}
} else {
let provides = currentInstance._provided
// 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._provided
if (parentProvides === provides) {
provides = currentInstance._provided = Object.create(parentProvides)
}
// TS doesn't allow symbol as index type
provides[key as string] = value
resolveProvided(currentInstance)[key as string] = value
}
}
export function resolveProvided(vm: Component): Record<string, any> {
// 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 existing = vm._provided
const parentProvides = vm.$parent && vm.$parent._provided
if (parentProvides === existing) {
return (vm._provided = Object.create(parentProvides))
} else {
return existing
}
}

View File

@ -1,6 +1,6 @@
import Vue from 'vue'
import { Observer } from 'core/observer/index'
import { isNative, isObject, hasOwn } from 'core/util/index'
import { isNative, isObject, hasOwn, nextTick } from 'core/util/index'
import testObjectOption from '../../../helpers/test-object-option'
describe('Options provide/inject', () => {
@ -677,4 +677,39 @@ describe('Options provide/inject', () => {
})
expect(`Injection "constructor" not found`).toHaveBeenWarned()
})
// #12667
test('provide with getters', async () => {
const spy = vi.fn()
const Child = {
render() {},
inject: ['foo'],
mounted() {
spy(this.foo)
}
}
let val = 1
const vm = new Vue({
components: { Child },
template: `<Child v-if="ok" />`,
data() {
return {
ok: false
}
},
provide() {
return {
get foo() {
return val
}
}
}
}).$mount()
val = 2
vm.ok = true
await nextTick()
expect(spy).toHaveBeenCalledWith(2)
})
})