]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(inject): should auto unwrap injected refs
authorEvan You <yyx990803@gmail.com>
Tue, 27 Jul 2021 21:52:31 +0000 (17:52 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 27 Jul 2021 21:52:37 +0000 (17:52 -0400)
fix #4196

packages/runtime-core/__tests__/apiOptions.spec.ts
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/componentOptions.ts
packages/runtime-dom/__tests__/customElement.spec.ts

index 10ede8a463886e9457b0d789aeb15520111bbd1a..9c50664b786e4df32d53bb831b1d4090afaf2483 100644 (file)
@@ -9,7 +9,8 @@ import {
   renderToString,
   ref,
   defineComponent,
-  createApp
+  createApp,
+  computed
 } from '@vue/runtime-test'
 
 describe('api: options', () => {
@@ -426,6 +427,69 @@ describe('api: options', () => {
     expect(renderToString(h(Root))).toBe(`1111234522`)
   })
 
+  test('provide/inject refs', async () => {
+    const n = ref(0)
+    const np = computed(() => n.value + 1)
+    const Parent = defineComponent({
+      provide() {
+        return {
+          n,
+          np
+        }
+      },
+      render: () => h(Child)
+    })
+    const Child = defineComponent({
+      inject: ['n', 'np'],
+      render(this: any) {
+        return this.n + this.np
+      }
+    })
+    const app = createApp(Parent)
+    // TODO remove in 3.3
+    app.config.unwrapInjectedRef = true
+    const root = nodeOps.createElement('div')
+    app.mount(root)
+    expect(serializeInner(root)).toBe(`1`)
+
+    n.value++
+    await nextTick()
+    expect(serializeInner(root)).toBe(`3`)
+  })
+
+  // TODO remove in 3.3
+  test('provide/inject refs (compat)', async () => {
+    const n = ref(0)
+    const np = computed(() => n.value + 1)
+    const Parent = defineComponent({
+      provide() {
+        return {
+          n,
+          np
+        }
+      },
+      render: () => h(Child)
+    })
+    const Child = defineComponent({
+      inject: ['n', 'np'],
+      render(this: any) {
+        return this.n.value + this.np.value
+      }
+    })
+    const app = createApp(Parent)
+
+    const root = nodeOps.createElement('div')
+    app.mount(root)
+    expect(serializeInner(root)).toBe(`1`)
+
+    n.value++
+    await nextTick()
+    expect(serializeInner(root)).toBe(`3`)
+
+    expect(`injected property "n" is a ref`).toHaveBeenWarned()
+    expect(`injected property "np" is a ref`).toHaveBeenWarned()
+  })
+
   test('provide accessing data in extends', () => {
     const Base = defineComponent({
       data() {
index a22ec8c0d7f280720c251d8193fb4e929c86f8a2..3e438107da03752d274aba6749dab2b694638ab6 100644 (file)
@@ -81,16 +81,22 @@ export interface AppConfig {
     trace: string
   ) => void
 
+  /**
+   * Options to pass to @vue/compiler-dom.
+   * Only supported in runtime compiler build.
+   */
+  compilerOptions: RuntimeCompilerOptions
+
   /**
    * @deprecated use config.compilerOptions.isCustomElement
    */
   isCustomElement?: (tag: string) => boolean
 
   /**
-   * Options to pass to @vue/compiler-dom.
-   * Only supported in runtime compiler build.
+   * Temporary config for opt-in to unwrap injected refs.
+   * TODO deprecate in 3.3
    */
-  compilerOptions: RuntimeCompilerOptions
+  unwrapInjectedRef?: boolean
 }
 
 export interface AppContext {
index bffb215cd5cb003e76bcaeb217146f650d8d9832..c10ef97d0e3d5199fd7efd0fb396ffbdbbea5918 100644 (file)
@@ -17,7 +17,7 @@ import {
   NOOP,
   isPromise
 } from '@vue/shared'
-import { computed } from '@vue/reactivity'
+import { computed, isRef, Ref } from '@vue/reactivity'
 import {
   watch,
   WatchOptions,
@@ -607,15 +607,21 @@ export function applyOptions(instance: ComponentInternalInstance) {
   // - watch (deferred since it relies on `this` access)
 
   if (injectOptions) {
-    resolveInjections(injectOptions, ctx, checkDuplicateProperties)
+    resolveInjections(
+      injectOptions,
+      ctx,
+      checkDuplicateProperties,
+      instance.appContext.config.unwrapInjectedRef
+    )
   }
 
   if (methods) {
     for (const key in methods) {
       const methodHandler = (methods as MethodOptions)[key]
       if (isFunction(methodHandler)) {
-        // In dev mode, we use the `createRenderContext` function to define methods to the proxy target,
-        // and those are read-only but reconfigurable, so it needs to be redefined here
+        // In dev mode, we use the `createRenderContext` function to define
+        // methods to the proxy target, and those are read-only but
+        // reconfigurable, so it needs to be redefined here
         if (__DEV__) {
           Object.defineProperty(ctx, key, {
             value: methodHandler.bind(publicThis),
@@ -810,25 +816,51 @@ export function applyOptions(instance: ComponentInternalInstance) {
 export function resolveInjections(
   injectOptions: ComponentInjectOptions,
   ctx: any,
-  checkDuplicateProperties = NOOP as any
+  checkDuplicateProperties = NOOP as any,
+  unwrapRef = false
 ) {
   if (isArray(injectOptions)) {
     injectOptions = normalizeInject(injectOptions)!
   }
   for (const key in injectOptions) {
     const opt = (injectOptions as ObjectInjectOptions)[key]
+    let injected: unknown
     if (isObject(opt)) {
       if ('default' in opt) {
-        ctx[key] = inject(
+        injected = inject(
           opt.from || key,
           opt.default,
           true /* treat default function as factory */
         )
       } else {
-        ctx[key] = inject(opt.from || key)
+        injected = inject(opt.from || key)
+      }
+    } else {
+      injected = inject(opt)
+    }
+    if (isRef(injected)) {
+      // TODO remove the check in 3.3
+      if (unwrapRef) {
+        Object.defineProperty(ctx, key, {
+          enumerable: true,
+          configurable: true,
+          get: () => (injected as Ref).value,
+          set: v => ((injected as Ref).value = v)
+        })
+      } else {
+        if (__DEV__) {
+          warn(
+            `injected property "${key}" is a ref and will be auto-unwrapped ` +
+              `and no longer needs \`.value\` in the next minor release. ` +
+              `To opt-in to the new behavior now, ` +
+              `set \`app.config.unwrapInjectedRef = true\` (this config is ` +
+              `temporary and will not be needed in the future.)`
+          )
+        }
+        ctx[key] = injected
       }
     } else {
-      ctx[key] = inject(opt)
+      ctx[key] = injected
     }
     if (__DEV__) {
       checkDuplicateProperties!(OptionTypes.INJECT, key)
index 1a4437ed360e0b082829589c18f8324e04967a4f..e2e444073f085ad38f0d01a0e0755ea0dcb43734 100644 (file)
@@ -1,7 +1,9 @@
 import {
   defineCustomElement,
   h,
+  inject,
   nextTick,
+  Ref,
   ref,
   renderSlot,
   VueElement
@@ -231,9 +233,9 @@ describe('defineCustomElement', () => {
 
   describe('provide/inject', () => {
     const Consumer = defineCustomElement({
-      inject: ['foo'],
-      render(this: any) {
-        return h('div', this.foo.value)
+      setup() {
+        const foo = inject<Ref>('foo')!
+        return () => h('div', foo.value)
       }
     })
     customElements.define('my-consumer', Consumer)