From ea5d0f3fbfd983cb0275457cbcef344f926381ea Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 16 Jul 2022 21:33:48 +0800 Subject: [PATCH] fix(inject): fix edge case of provided with async-mutated getters fix #12667 --- src/core/instance/inject.ts | 15 ++++++--- src/v3/apiInject.ts | 29 ++++++++++-------- test/unit/features/options/inject.spec.ts | 37 ++++++++++++++++++++++- 3 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/core/instance/inject.ts b/src/core/instance/inject.ts index 4c18909de8f..d5fa2b5ba71 100644 --- a/src/core/instance/inject.ts +++ b/src/core/instance/inject.ts @@ -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() } } diff --git a/src/v3/apiInject.ts b/src/v3/apiInject.ts index 3644c56ab46..76928268b13 100644 --- a/src/v3/apiInject.ts +++ b/src/v3/apiInject.ts @@ -1,5 +1,6 @@ import { isFunction, warn } from 'core/util' import { currentInstance } from './currentInstance' +import type { Component } from 'types/component' export interface InjectionKey extends Symbol {} @@ -9,19 +10,23 @@ export function provide(key: InjectionKey | 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 { + // 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 } } diff --git a/test/unit/features/options/inject.spec.ts b/test/unit/features/options/inject.spec.ts index 399061f9768..983367b6126 100644 --- a/test/unit/features/options/inject.spec.ts +++ b/test/unit/features/options/inject.spec.ts @@ -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: ``, + data() { + return { + ok: false + } + }, + provide() { + return { + get foo() { + return val + } + } + } + }).$mount() + + val = 2 + vm.ok = true + await nextTick() + expect(spy).toHaveBeenCalledWith(2) + }) })