diff --git a/packages/runtime-core/src/compat/component.ts b/packages/runtime-core/src/compat/component.ts index 6168c3e7..c67c681f 100644 --- a/packages/runtime-core/src/compat/component.ts +++ b/packages/runtime-core/src/compat/component.ts @@ -1,16 +1,25 @@ import { isArray, isFunction, isObject, isPromise } from '@vue/shared' import { defineAsyncComponent } from '../apiAsyncComponent' -import { Component, ComponentOptions, FunctionalComponent } from '../component' +import { + Component, + ComponentOptions, + FunctionalComponent, + getCurrentInstance +} from '../component' +import { resolveInjections } from '../componentOptions' +import { InternalSlots } from '../componentSlots' import { isVNode } from '../vnode' -import { softAssertCompatEnabled } from './compatConfig' -import { DeprecationTypes } from './deprecations' +import { isCompatEnabled, softAssertCompatEnabled } from './compatConfig' +import { DeprecationTypes, warnDeprecation } from './deprecations' +import { getCompatListeners } from './instanceListeners' +import { compatH } from './renderFn' export function convertLegacyComponent(comp: any): Component { // 2.x async component - if ( - isFunction(comp) && - softAssertCompatEnabled(DeprecationTypes.COMPONENT_ASYNC, comp) - ) { + // since after disabling this, plain functions are still valid usage, do not + // use softAssert here. + if (isFunction(comp) && isCompatEnabled(DeprecationTypes.COMPONENT_ASYNC)) { + __DEV__ && warnDeprecation(DeprecationTypes.COMPONENT_ASYNC, comp) return convertLegacyAsyncComponent(comp) } @@ -78,6 +87,56 @@ function convertLegacyAsyncComponent(comp: LegacyAsyncComponent) { return converted } -function convertLegacyFunctionalComponent(comp: ComponentOptions) { - return comp.render as FunctionalComponent +const normalizedFunctionalComponentMap = new Map< + ComponentOptions, + FunctionalComponent +>() + +const legacySlotProxyHandlers: ProxyHandler = { + get(target, key: string) { + const slot = target[key] + return slot && slot() + } +} + +function convertLegacyFunctionalComponent(comp: ComponentOptions) { + if (normalizedFunctionalComponentMap.has(comp)) { + return normalizedFunctionalComponentMap.get(comp)! + } + + const legacyFn = comp.render as any + + const Func: FunctionalComponent = (props, ctx) => { + const instance = getCurrentInstance()! + + const legacyCtx = { + props, + children: instance.vnode.children || [], + data: instance.vnode.props || {}, + scopedSlots: ctx.slots, + parent: instance.parent && instance.parent.proxy, + get slots() { + return new Proxy(ctx.slots, legacySlotProxyHandlers) + }, + get listeners() { + return getCompatListeners(instance) + }, + get injections() { + if (comp.inject) { + const injections = {} + resolveInjections(comp.inject, {}) + return injections + } + return {} + } + } + return legacyFn(compatH, legacyCtx) + } + Func.props = comp.props + Func.displayName = comp.name + // v2 functional components do not inherit attrs + Func.inheritAttrs = false + + normalizedFunctionalComponentMap.set(comp, Func) + return Func } diff --git a/packages/runtime-core/src/compat/deprecations.ts b/packages/runtime-core/src/compat/deprecations.ts index c2407af5..58949876 100644 --- a/packages/runtime-core/src/compat/deprecations.ts +++ b/packages/runtime-core/src/compat/deprecations.ts @@ -314,7 +314,13 @@ const deprecationData: Record = { name ? ` <${name}>` : `s` } should be explicitly created via \`defineAsyncComponent()\` ` + `in Vue 3. Plain functions will be treated as functional components in ` + - `non-compat build.` + `non-compat build. If you have already migrated all async component ` + + `usage and intend to use plain functions for functional components, ` + + `you can disable the compat behavior and suppress this ` + + `warning with:` + + `\n\n configureCompat({ ${ + DeprecationTypes.COMPONENT_ASYNC + }: false })\n` ) }, link: `https://v3.vuejs.org/guide/migration/async-components.html` @@ -327,13 +333,10 @@ const deprecationData: Record = { `Functional component${ name ? ` <${name}>` : `s` } should be defined as a plain function in Vue 3. The "functional" ` + - `option has been removed.\n` + - `NOTE: Before migrating, ensure that all async ` + - `components have been upgraded to use \`defineAsyncComponent()\` and ` + - `then disable compat for legacy async components with:` + - `\n\n configureCompat({ ${ - DeprecationTypes.COMPONENT_ASYNC - }: false })\n` + `option has been removed. NOTE: Before migrating to use plain ` + + `functions for functional components, first make sure that all async ` + + `components usage have been migrated and its compat behavior has ` + + `been disabled.` ) }, link: `https://v3.vuejs.org/guide/migration/functional-components.html` diff --git a/packages/runtime-core/src/compat/renderFn.ts b/packages/runtime-core/src/compat/renderFn.ts index b9074af1..ef4cd37b 100644 --- a/packages/runtime-core/src/compat/renderFn.ts +++ b/packages/runtime-core/src/compat/renderFn.ts @@ -41,17 +41,21 @@ type LegacyVNodeChildren = | VNode | VNodeArrayChildren -export function h( +export function compatH( type: string | Component, children?: LegacyVNodeChildren ): VNode -export function h( +export function compatH( type: string | Component, props?: LegacyVNodeProps, children?: LegacyVNodeChildren ): VNode -export function h(type: any, propsOrChildren?: any, children?: any): VNode { +export function compatH( + type: any, + propsOrChildren?: any, + children?: any +): VNode { const l = arguments.length if (l === 2) { if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { @@ -85,7 +89,7 @@ export function h(type: any, propsOrChildren?: any, children?: any): VNode { function convertLegacyProps(props: LegacyVNodeProps): Data & VNodeProps { // TODO - return {} + return props as any } function convertLegacyDirectives(vnode: VNode, props: LegacyVNodeProps): VNode { diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index 4a93c697..1f182c6d 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -597,31 +597,7 @@ export function applyOptions( // - watch (deferred since it relies on `this` access) if (injectOptions) { - if (isArray(injectOptions)) { - for (let i = 0; i < injectOptions.length; i++) { - const key = injectOptions[i] - ctx[key] = inject(key) - if (__DEV__) { - checkDuplicateProperties!(OptionTypes.INJECT, key) - } - } - } else { - for (const key in injectOptions) { - const opt = injectOptions[key] - if (isObject(opt)) { - ctx[key] = inject( - opt.from || key, - opt.default, - true /* treat default function as factory */ - ) - } else { - ctx[key] = inject(opt) - } - if (__DEV__) { - checkDuplicateProperties!(OptionTypes.INJECT, key) - } - } - } + resolveInjections(injectOptions, ctx, checkDuplicateProperties) } if (methods) { @@ -842,6 +818,38 @@ export function applyOptions( } } +export function resolveInjections( + injectOptions: ComponentInjectOptions, + ctx: any, + checkDuplicateProperties = NOOP as any +) { + if (isArray(injectOptions)) { + for (let i = 0; i < injectOptions.length; i++) { + const key = injectOptions[i] + ctx[key] = inject(key) + if (__DEV__) { + checkDuplicateProperties!(OptionTypes.INJECT, key) + } + } + } else { + for (const key in injectOptions) { + const opt = injectOptions[key] + if (isObject(opt)) { + ctx[key] = inject( + opt.from || key, + opt.default, + true /* treat default function as factory */ + ) + } else { + ctx[key] = inject(opt) + } + if (__DEV__) { + checkDuplicateProperties!(OptionTypes.INJECT, key) + } + } + } +} + function callSyncHook( name: 'beforeCreate' | 'created', type: LifecycleHooks,