diff --git a/packages/runtime-core/__tests__/componentSlots.spec.ts b/packages/runtime-core/__tests__/componentSlots.spec.ts index ecb548f21..9248a8e6a 100644 --- a/packages/runtime-core/__tests__/componentSlots.spec.ts +++ b/packages/runtime-core/__tests__/componentSlots.spec.ts @@ -82,7 +82,7 @@ describe('component: slots', () => { expect(slots.default()).toMatchObject([normalizeVNode(h('span'))]) }) - test('updateSlots: instance.slots should be update correctly (when slotType is number)', async () => { + test('updateSlots: instance.slots should be updated correctly (when slotType is number)', async () => { const flag1 = ref(true) let instance: any @@ -124,7 +124,7 @@ describe('component: slots', () => { expect(instance.slots).toHaveProperty('two') }) - test('updateSlots: instance.slots should be update correctly (when slotType is null)', async () => { + test('updateSlots: instance.slots should be updated correctly (when slotType is null)', async () => { const flag1 = ref(true) let instance: any diff --git a/packages/runtime-core/src/compat/compatConfig.ts b/packages/runtime-core/src/compat/compatConfig.ts index 25a387f1d..50f7efd69 100644 --- a/packages/runtime-core/src/compat/compatConfig.ts +++ b/packages/runtime-core/src/compat/compatConfig.ts @@ -262,7 +262,8 @@ const deprecationData: Record = { [DeprecationTypes.PROPS_DEFAULT_THIS]: { message: (key: string) => - `props default value function no longer has access to "this". ` + + `props default value function no longer has access to "this". The compat ` + + `build only offers access to this.$options.` + `(found in prop "${key}")`, link: `https://v3.vuejs.org/guide/migration/props-default-this.html` }, diff --git a/packages/runtime-core/src/compat/component.ts b/packages/runtime-core/src/compat/component.ts index 0cae7ad38..36188d0ce 100644 --- a/packages/runtime-core/src/compat/component.ts +++ b/packages/runtime-core/src/compat/component.ts @@ -132,7 +132,7 @@ function convertLegacyFunctionalComponent(comp: ComponentOptions) { data: instance.vnode.props || {}, scopedSlots: ctx.slots, parent: instance.parent && instance.parent.proxy, - get slots() { + slots() { return new Proxy(ctx.slots, legacySlotProxyHandlers) }, get listeners() { diff --git a/packages/runtime-core/src/compat/instance.ts b/packages/runtime-core/src/compat/instance.ts index b324d7f70..8466d0cd6 100644 --- a/packages/runtime-core/src/compat/instance.ts +++ b/packages/runtime-core/src/compat/instance.ts @@ -12,13 +12,16 @@ import { getCompatListeners } from './instanceListeners' import { shallowReadonly } from '@vue/reactivity' import { legacySlotProxyHandlers } from './component' import { compatH } from './renderFn' -import { - legacyBindObjectProps, - legacyRenderSlot, - legacyRenderStatic -} from './renderHelpers' import { createCommentVNode, createTextVNode } from '../vnode' import { renderList } from '../helpers/renderList' +import { + legacyBindObjectListeners, + legacyBindObjectProps, + legacyCheckKeyCodes, + legacyRenderSlot, + legacyRenderStatic, + legacyresolveScopedSlots +} from './renderHelpers' export function installCompatInstanceProperties(map: PublicPropertiesMap) { const set = (target: any, key: any, val: any) => { @@ -98,6 +101,9 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) { _b: () => legacyBindObjectProps, _e: () => createCommentVNode, _v: () => createTextVNode, - _m: i => legacyRenderStatic.bind(null, i) + _m: i => legacyRenderStatic.bind(null, i), + _g: () => legacyBindObjectListeners, + _u: () => legacyresolveScopedSlots, + _k: i => legacyCheckKeyCodes.bind(null, i) } as PublicPropertiesMap) } diff --git a/packages/runtime-core/src/compat/props.ts b/packages/runtime-core/src/compat/props.ts index 51871562d..e2fccb667 100644 --- a/packages/runtime-core/src/compat/props.ts +++ b/packages/runtime-core/src/compat/props.ts @@ -1,11 +1,39 @@ +import { isArray } from '@vue/shared' +import { inject } from '../apiInject' +import { ComponentInternalInstance, Data } from '../component' +import { ComponentOptions, resolveMergedOptions } from '../componentOptions' import { DeprecationTypes, warnDeprecation } from './compatConfig' -export function createPropsDefaultThis(propKey: string) { +export function createPropsDefaultThis( + instance: ComponentInternalInstance, + rawProps: Data, + propKey: string +) { return new Proxy( {}, { - get() { - warnDeprecation(DeprecationTypes.PROPS_DEFAULT_THIS, null, propKey) + get(_, key: string) { + __DEV__ && + warnDeprecation(DeprecationTypes.PROPS_DEFAULT_THIS, null, propKey) + // $options + if (key === '$options') { + return resolveMergedOptions(instance) + } + // props + if (key in rawProps) { + return rawProps[key] + } + // injections + const injections = (instance.type as ComponentOptions).inject + if (injections) { + if (isArray(injections)) { + if (injections.includes(key)) { + return inject(key) + } + } else if (key in injections) { + return inject(key) + } + } } } ) diff --git a/packages/runtime-core/src/compat/renderFn.ts b/packages/runtime-core/src/compat/renderFn.ts index 29180f633..b04ca0cda 100644 --- a/packages/runtime-core/src/compat/renderFn.ts +++ b/packages/runtime-core/src/compat/renderFn.ts @@ -1,7 +1,9 @@ import { extend, + hyphenate, isArray, isObject, + makeMap, normalizeClass, normalizeStyle, ShapeFlags, @@ -14,6 +16,7 @@ import { Data, InternalRenderFunction } from '../component' +import { currentRenderingInstance } from '../componentRenderContext' import { DirectiveArguments, withDirectives } from '../directives' import { resolveDirective, @@ -27,7 +30,12 @@ import { VNodeArrayChildren, VNodeProps } from '../vnode' -import { checkCompatEnabled, DeprecationTypes } from './compatConfig' +import { + checkCompatEnabled, + DeprecationTypes, + isCompatEnabled +} from './compatConfig' +import { compatModelEventPrefix } from './vModel' const v3CompiledRenderFnRE = /^(?:function \w+)?\(_ctx, _cache/ @@ -76,6 +84,11 @@ interface LegacyVNodeProps { props?: Record slot?: string scopedSlots?: Record + model?: { + value: any + callback: (v: any) => void + expression: string + } } interface LegacyVNodeDirective { @@ -107,12 +120,22 @@ export function compatH( propsOrChildren?: any, children?: any ): VNode { - // to support v2 string component name lookup - type = resolveDynamicComponent(type) + // to support v2 string component name look!up + if (typeof type === 'string') { + const t = hyphenate(type) + if (t === 'transition' || t === 'transition-group' || t === 'keep-alive') { + // since transition and transition-group are runtime-dom-specific, + // we cannot import them directly here. Instead they are registered using + // special keys in @vue/compat entry. + type = `__compat__${t}` + } + type = resolveDynamicComponent(type) + } const l = arguments.length - if (l === 2) { - if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { + const is2ndArgArrayChildren = isArray(propsOrChildren) + if (l === 2 || is2ndArgArrayChildren) { + if (isObject(propsOrChildren) && !is2ndArgArrayChildren) { // single vnode without props if (isVNode(propsOrChildren)) { return convertLegacySlots(createVNode(type, null, [propsOrChildren])) @@ -120,7 +143,7 @@ export function compatH( // props without children return convertLegacySlots( convertLegacyDirectives( - createVNode(type, convertLegacyProps(propsOrChildren)), + createVNode(type, convertLegacyProps(propsOrChildren, type)), propsOrChildren ) ) @@ -134,15 +157,20 @@ export function compatH( } return convertLegacySlots( convertLegacyDirectives( - createVNode(type, convertLegacyProps(propsOrChildren), children), + createVNode(type, convertLegacyProps(propsOrChildren, type), children), propsOrChildren ) ) } } +const skipLegacyRootLevelProps = /*#__PURE__*/ makeMap( + 'refInFor,staticStyle,staticClass,directives,model' +) + function convertLegacyProps( - legacyProps?: LegacyVNodeProps + legacyProps: LegacyVNodeProps | undefined, + type: any ): Data & VNodeProps | null { if (!legacyProps) { return null @@ -172,11 +200,7 @@ function convertLegacyProps( } } } - } else if ( - key !== 'refInFor' && - key !== 'staticStyle' && - key !== 'staticClass' - ) { + } else if (!skipLegacyRootLevelProps(key)) { converted[key] = legacyProps[key as keyof LegacyVNodeProps] } } @@ -188,6 +212,13 @@ function convertLegacyProps( converted.style = normalizeStyle([legacyProps.staticStyle, converted.style]) } + if (legacyProps.model && isObject(type)) { + // v2 compiled component v-model + const { prop = 'value', event = 'input' } = (type as any).model || {} + converted[prop] = legacyProps.model.value + converted[compatModelEventPrefix + event] = legacyProps.model.callback + } + return converted } @@ -237,7 +268,12 @@ function convertLegacySlots(vnode: VNode): VNode { const child = children[i] const slotName = (isVNode(child) && child.props && child.props.slot) || 'default' - ;(slots[slotName] || (slots[slotName] = [] as any[])).push(child) + const slot = slots[slotName] || (slots[slotName] = [] as any[]) + if (isVNode(child) && child.type === 'template') { + slot.push(child.children) + } else { + slot.push(child) + } } if (slots) { for (const key in slots) { @@ -263,3 +299,31 @@ function convertLegacySlots(vnode: VNode): VNode { return vnode } + +export function defineLegacyVNodeProperties(vnode: VNode) { + if ( + isCompatEnabled(DeprecationTypes.RENDER_FUNCTION, currentRenderingInstance) + ) { + const getInstance = () => vnode.component && vnode.component.proxy + let componentOptions: any + Object.defineProperties(vnode, { + elm: { get: () => vnode.el }, + componentInstance: { get: getInstance }, + child: { get: getInstance }, + componentOptions: { + get: () => { + if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { + if (componentOptions) { + return componentOptions + } + return (componentOptions = { + Ctor: vnode.type, + propsData: vnode.props, + children: vnode.children + }) + } + } + } + }) + } +} diff --git a/packages/runtime-core/src/compat/renderHelpers.ts b/packages/runtime-core/src/compat/renderHelpers.ts index 0f38a245a..0b90632dc 100644 --- a/packages/runtime-core/src/compat/renderHelpers.ts +++ b/packages/runtime-core/src/compat/renderHelpers.ts @@ -8,9 +8,22 @@ import { normalizeClass } from '@vue/shared' import { ComponentInternalInstance } from '../component' +import { Slot } from '../componentSlots' +import { createSlots } from '../helpers/createSlots' import { renderSlot } from '../helpers/renderSlot' +import { toHandlers } from '../helpers/toHandlers' import { mergeProps, VNode } from '../vnode' +function toObject(arr: Array): Object { + const res = {} + for (let i = 0; i < arr.length; i++) { + if (arr[i]) { + extend(res, arr[i]) + } + } + return res +} + export function legacyBindObjectProps( data: any, _tag: string, @@ -49,6 +62,10 @@ export function legacyBindObjectProps( return data } +export function legacyBindObjectListeners(props: any, listeners: any) { + return mergeProps(props, toHandlers(listeners)) +} + export function legacyRenderSlot( instance: ComponentInternalInstance, name: string, @@ -62,6 +79,41 @@ export function legacyRenderSlot( return renderSlot(instance.slots, name, props, fallback && (() => fallback)) } +type LegacyScopedSlotsData = Array< + | { + key: string + fn: Function + } + | LegacyScopedSlotsData +> + +export function legacyresolveScopedSlots( + fns: LegacyScopedSlotsData, + raw?: Record, + // the following are added in 2.6 + hasDynamicKeys?: boolean +) { + // v2 default slot doesn't have name + return createSlots( + raw || ({ $stable: !hasDynamicKeys } as any), + mapKeyToName(fns) + ) +} + +function mapKeyToName(slots: LegacyScopedSlotsData) { + for (let i = 0; i < slots.length; i++) { + const fn = slots[i] + if (fn) { + if (isArray(fn)) { + mapKeyToName(fn) + } else { + ;(fn as any).name = fn.key || 'default' + } + } + } + return slots as any +} + const staticCacheMap = /*#__PURE__*/ new WeakMap< ComponentInternalInstance, any[] @@ -83,12 +135,30 @@ export function legacyRenderStatic( return (cache[index] = fn.call(ctx, null, ctx)) } -function toObject(arr: Array): Object { - const res = {} - for (let i = 0; i < arr.length; i++) { - if (arr[i]) { - extend(res, arr[i]) - } +export function legacyCheckKeyCodes( + instance: ComponentInternalInstance, + eventKeyCode: number, + key: string, + builtInKeyCode?: number | number[], + eventKeyName?: string, + builtInKeyName?: string | string[] +) { + const config = instance.appContext.config as any + const configKeyCodes = config.keyCodes || {} + const mappedKeyCode = configKeyCodes[key] || builtInKeyCode + if (builtInKeyName && eventKeyName && !configKeyCodes[key]) { + return isKeyNotMatch(builtInKeyName, eventKeyName) + } else if (mappedKeyCode) { + return isKeyNotMatch(mappedKeyCode, eventKeyCode) + } else if (eventKeyName) { + return hyphenate(eventKeyName) !== key + } +} + +function isKeyNotMatch(expect: T | T[], actual: T): boolean { + if (isArray(expect)) { + return expect.indexOf(actual) === -1 + } else { + return expect !== actual } - return res } diff --git a/packages/runtime-core/src/compat/vModel.ts b/packages/runtime-core/src/compat/vModel.ts index d24f06b62..03ae787af 100644 --- a/packages/runtime-core/src/compat/vModel.ts +++ b/packages/runtime-core/src/compat/vModel.ts @@ -35,6 +35,9 @@ export function convertLegacyVModelProps(vnode: VNode) { warnedTypes.add(type as ComponentOptions) } + // v3 compiled model code -> v2 compat props + // modelValue -> value + // onUpdate:modelValue -> onModelCompat:input const { prop = 'value', event = 'input' } = (type as any).model || {} props[prop] = props.modelValue delete props.modelValue @@ -42,7 +45,6 @@ export function convertLegacyVModelProps(vnode: VNode) { if (dynamicProps) { dynamicProps[dynamicProps.indexOf('modelValue')] = prop } - props[compatModelEventPrefix + event] = props['onUpdate:modelValue'] delete props['onUpdate:modelValue'] } diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index ef08c3ad4..ed933056c 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -376,9 +376,8 @@ function resolvePropValue( setCurrentInstance(instance) value = propsDefaults[key] = defaultValue.call( __COMPAT__ && - __DEV__ && isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance) - ? createPropsDefaultThis(key) + ? createPropsDefaultThis(instance, props, key) : null, props ) diff --git a/packages/runtime-core/src/componentRenderContext.ts b/packages/runtime-core/src/componentRenderContext.ts index 1771d9aa0..6cc232358 100644 --- a/packages/runtime-core/src/componentRenderContext.ts +++ b/packages/runtime-core/src/componentRenderContext.ts @@ -25,6 +25,10 @@ export function setCurrentRenderingInstance( const prev = currentRenderingInstance currentRenderingInstance = instance currentScopeId = (instance && instance.type.__scopeId) || null + // v2 pre-compiled components uses _scopeId instead of __scopeId + if (__COMPAT__ && !currentScopeId) { + currentScopeId = (instance && (instance.type as any)._scopeId) || null + } return prev } diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts index ab9b1fbc6..1acbd1aa1 100644 --- a/packages/runtime-core/src/componentSlots.ts +++ b/packages/runtime-core/src/componentSlots.ts @@ -19,6 +19,7 @@ import { warn } from './warning' import { isKeepAlive } from './components/KeepAlive' import { withCtx } from './componentRenderContext' import { isHmrUpdating } from './hmr' +import { DeprecationTypes, isCompatEnabled } from './compat/compatConfig' export type Slot = (...args: any[]) => VNode[] @@ -72,7 +73,11 @@ const normalizeSlot = ( return normalizeSlotValue(rawSlot(props)) }, ctx) as Slot -const normalizeObjectSlots = (rawSlots: RawSlots, slots: InternalSlots) => { +const normalizeObjectSlots = ( + rawSlots: RawSlots, + slots: InternalSlots, + instance: ComponentInternalInstance +) => { const ctx = rawSlots._ctx for (const key in rawSlots) { if (isInternalKey(key)) continue @@ -80,7 +85,13 @@ const normalizeObjectSlots = (rawSlots: RawSlots, slots: InternalSlots) => { if (isFunction(value)) { slots[key] = normalizeSlot(key, value, ctx) } else if (value != null) { - if (__DEV__ && !__COMPAT__) { + if ( + __DEV__ && + !( + __COMPAT__ && + isCompatEnabled(DeprecationTypes.RENDER_FUNCTION, instance) + ) + ) { warn( `Non-function value encountered for slot "${key}". ` + `Prefer function slots for better performance.` @@ -117,7 +128,11 @@ export const initSlots = ( // make compiler marker non-enumerable def(children as InternalSlots, '_', type) } else { - normalizeObjectSlots(children as RawSlots, (instance.slots = {})) + normalizeObjectSlots( + children as RawSlots, + (instance.slots = {}), + instance + ) } } else { instance.slots = {} @@ -162,7 +177,7 @@ export const updateSlots = ( } } else { needDeletionCheck = !(children as RawSlots).$stable - normalizeObjectSlots(children as RawSlots, slots) + normalizeObjectSlots(children as RawSlots, slots, instance) } deletionComparisonTarget = children as RawSlots } else if (children) { diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 9d1fd791a..05fc6ad07 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -43,6 +43,7 @@ import { hmrDirtyComponents } from './hmr' import { setCompiledSlotRendering } from './helpers/renderSlot' import { convertLegacyComponent } from './compat/component' import { convertLegacyVModelProps } from './compat/vModel' +import { defineLegacyVNodeProperties } from './compat/renderFn' export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as { __isFragment: true @@ -472,6 +473,7 @@ function _createVNode( if (__COMPAT__) { convertLegacyVModelProps(vnode) + defineLegacyVNodeProperties(vnode) } return vnode @@ -486,7 +488,7 @@ export function cloneVNode( // key enumeration cost. const { props, ref, patchFlag, children } = vnode const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props - return { + const cloned: VNode = { __v_isVNode: true, [ReactiveFlags.SKIP]: true, type: vnode.type, @@ -540,6 +542,10 @@ export function cloneVNode( el: vnode.el, anchor: vnode.anchor } + if (__COMPAT__) { + defineLegacyVNodeProperties(cloned) + } + return cloned as any } /** diff --git a/packages/vue-compat/src/index.ts b/packages/vue-compat/src/index.ts index 845736352..6421b939e 100644 --- a/packages/vue-compat/src/index.ts +++ b/packages/vue-compat/src/index.ts @@ -2,16 +2,11 @@ // and the compiler, and supports on-the-fly compilation of the template option. import { initDev } from './dev' import { compile, CompilerError, CompilerOptions } from '@vue/compiler-dom' -import { - registerRuntimeCompiler, - RenderFunction, - warn, - createApp, - compatUtils -} from '@vue/runtime-dom' +import { registerRuntimeCompiler, RenderFunction, warn } from '@vue/runtime-dom' import { isString, NOOP, generateCodeFrame, extend } from '@vue/shared' import { InternalRenderFunction } from 'packages/runtime-core/src/component' import * as runtimeDom from '@vue/runtime-dom' +import Vue from './runtime' if (__DEV__) { initDev() @@ -92,10 +87,6 @@ function compileToFunction( registerRuntimeCompiler(compileToFunction) -const Vue = compatUtils.createCompatVue(createApp) - Vue.compile = compileToFunction -extend(Vue, runtimeDom) - export default Vue diff --git a/packages/vue-compat/src/runtime.ts b/packages/vue-compat/src/runtime.ts index a9f19cf70..38ee5cff2 100644 --- a/packages/vue-compat/src/runtime.ts +++ b/packages/vue-compat/src/runtime.ts @@ -1,7 +1,17 @@ // This entry exports the runtime only, and is built as // `dist/vue.esm-bundler.js` which is used by default for bundlers. import { initDev } from './dev' -import { compatUtils, createApp, warn } from '@vue/runtime-dom' +import { + compatUtils, + createApp, + warn, + Transition, + TransitionGroup, + KeepAlive, + DeprecationTypes, + vShow, + vModelDynamic +} from '@vue/runtime-dom' import { extend } from '@vue/shared' if (__DEV__) { @@ -10,7 +20,25 @@ if (__DEV__) { import * as runtimeDom from '@vue/runtime-dom' -const Vue = compatUtils.createCompatVue(createApp) +function wrappedCreateApp(...args: any[]) { + // @ts-ignore + const app = createApp(...args) + if (compatUtils.isCompatEnabled(DeprecationTypes.RENDER_FUNCTION, null)) { + // register built-in components so that they can be resolved via strings + // in the legacy h() call. The __compat__ prefix is to ensure that v3 h() + // doesn't get affected. + app.component('__compat__transition', Transition) + app.component('__compat__transition-group', TransitionGroup) + app.component('__compat__keep-alive', KeepAlive) + // built-in directives. No need for prefix since there's no render fn API + // for resolving directives via string in v3. + app._context.directives.show = vShow + app._context.directives.model = vModelDynamic + } + return app +} + +const Vue = compatUtils.createCompatVue(wrappedCreateApp) Vue.compile = (() => { if (__DEV__) { @@ -29,4 +57,7 @@ Vue.compile = (() => { extend(Vue, runtimeDom) +// @ts-ignore +Vue.createApp = wrappedCreateApp + export default Vue