fix(inject): should auto unwrap injected refs

fix #4196
This commit is contained in:
Evan You 2021-07-27 17:52:31 -04:00
parent 8681c12c0c
commit 561e210157
4 changed files with 119 additions and 15 deletions

View 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() {

View 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 {

View 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 {
ctx[key] = inject(opt)
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] = injected
}
if (__DEV__) {
checkDuplicateProperties!(OptionTypes.INJECT, key)

View 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)