Jelajahi Sumber

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

fix #12667
Evan You 3 tahun lalu
induk
melakukan
ea5d0f3fbf
3 mengubah file dengan 63 tambahan dan 18 penghapusan
  1. 10 5
      src/core/instance/inject.ts
  2. 17 12
      src/v3/apiInject.ts
  3. 36 1
      test/unit/features/options/inject.spec.ts

+ 10 - 5
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()
   }
 }
 

+ 17 - 12
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<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
   }
 }
 

+ 36 - 1
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: `<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)
+  })
 })