fix(compat): fix $options mutation + adjust private API initialization

close #10626
close #10636
This commit is contained in:
Evan You 2024-04-15 19:28:37 +08:00
parent 04af9504a7
commit d58d133b1c
No known key found for this signature in database
GPG Key ID: B9D421896CA450FB
3 changed files with 116 additions and 40 deletions

View File

@ -15,6 +15,7 @@ import {
DeprecationTypes, DeprecationTypes,
assertCompatEnabled, assertCompatEnabled,
isCompatEnabled, isCompatEnabled,
warnDeprecation,
} from './compatConfig' } from './compatConfig'
import { off, on, once } from './instanceEventEmitter' import { off, on, once } from './instanceEventEmitter'
import { getCompatListeners } from './instanceListeners' import { getCompatListeners } from './instanceListeners'
@ -121,50 +122,77 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) {
$children: getCompatChildren, $children: getCompatChildren,
$listeners: getCompatListeners, $listeners: getCompatListeners,
// inject additional properties into $options for compat
// e.g. vuex needs this.$options.parent
$options: i => {
if (!isCompatEnabled(DeprecationTypes.PRIVATE_APIS, i)) {
return resolveMergedOptions(i)
}
if (i.resolvedOptions) {
return i.resolvedOptions
}
const res = (i.resolvedOptions = extend({}, resolveMergedOptions(i)))
Object.defineProperties(res, {
parent: {
get() {
warnDeprecation(DeprecationTypes.PRIVATE_APIS, i, '$options.parent')
return i.proxy!.$parent
},
},
propsData: {
get() {
warnDeprecation(
DeprecationTypes.PRIVATE_APIS,
i,
'$options.propsData',
)
return i.vnode.props
},
},
})
return res
},
} as PublicPropertiesMap) } as PublicPropertiesMap)
/* istanbul ignore if */ const privateAPIs = {
if (isCompatEnabled(DeprecationTypes.PRIVATE_APIS, null)) { // needed by many libs / render fns
extend(map, { $vnode: i => i.vnode,
// needed by many libs / render fns
$vnode: i => i.vnode,
// inject additional properties into $options for compat // some private properties that are likely accessed...
// e.g. vuex needs this.$options.parent _self: i => i.proxy,
$options: i => { _uid: i => i.uid,
const res = extend({}, resolveMergedOptions(i)) _data: i => i.data,
res.parent = i.proxy!.$parent _isMounted: i => i.isMounted,
res.propsData = i.vnode.props _isDestroyed: i => i.isUnmounted,
return res
},
// some private properties that are likely accessed... // v2 render helpers
_self: i => i.proxy, $createElement: () => compatH,
_uid: i => i.uid, _c: () => compatH,
_data: i => i.data, _o: () => legacyMarkOnce,
_isMounted: i => i.isMounted, _n: () => looseToNumber,
_isDestroyed: i => i.isUnmounted, _s: () => toDisplayString,
_l: () => renderList,
_t: i => legacyRenderSlot.bind(null, i),
_q: () => looseEqual,
_i: () => looseIndexOf,
_m: i => legacyRenderStatic.bind(null, i),
_f: () => resolveFilter,
_k: i => legacyCheckKeyCodes.bind(null, i),
_b: () => legacyBindObjectProps,
_v: () => createTextVNode,
_e: () => createCommentVNode,
_u: () => legacyresolveScopedSlots,
_g: () => legacyBindObjectListeners,
_d: () => legacyBindDynamicKeys,
_p: () => legacyPrependModifier,
} as PublicPropertiesMap
// v2 render helpers for (const key in privateAPIs) {
$createElement: () => compatH, map[key] = i => {
_c: () => compatH, if (isCompatEnabled(DeprecationTypes.PRIVATE_APIS, i)) {
_o: () => legacyMarkOnce, return privateAPIs[key](i)
_n: () => looseToNumber, }
_s: () => toDisplayString, }
_l: () => renderList,
_t: i => legacyRenderSlot.bind(null, i),
_q: () => looseEqual,
_i: () => looseIndexOf,
_m: i => legacyRenderStatic.bind(null, i),
_f: () => resolveFilter,
_k: i => legacyCheckKeyCodes.bind(null, i),
_b: () => legacyBindObjectProps,
_v: () => createTextVNode,
_e: () => createCommentVNode,
_u: () => legacyresolveScopedSlots,
_g: () => legacyBindObjectListeners,
_d: () => legacyBindDynamicKeys,
_p: () => legacyPrependModifier,
} as PublicPropertiesMap)
} }
} }

View File

@ -45,6 +45,7 @@ import { type Directive, validateDirectiveName } from './directives'
import { import {
type ComponentOptions, type ComponentOptions,
type ComputedOptions, type ComputedOptions,
type MergedComponentOptions,
type MethodOptions, type MethodOptions,
applyOptions, applyOptions,
resolveMergedOptions, resolveMergedOptions,
@ -524,6 +525,12 @@ export interface ComponentInternalInstance {
* @internal * @internal
*/ */
getCssVars?: () => Record<string, string> getCssVars?: () => Record<string, string>
/**
* v2 compat only, for caching mutated $options
* @internal
*/
resolvedOptions?: MergedComponentOptions
} }
const emptyAppContext = createAppContext() const emptyAppContext = createAppContext()

View File

@ -14,6 +14,7 @@ beforeEach(() => {
Vue.configureCompat({ Vue.configureCompat({
MODE: 2, MODE: 2,
GLOBAL_MOUNT: 'suppress-warning', GLOBAL_MOUNT: 'suppress-warning',
PRIVATE_APIS: 'suppress-warning',
}) })
}) })
@ -331,3 +332,43 @@ test('INSTANCE_ATTR_CLASS_STYLE', () => {
)('Anonymous'), )('Anonymous'),
).toHaveBeenWarned() ).toHaveBeenWarned()
}) })
test('$options mutation', () => {
const Comp = {
props: ['id'],
template: '<div/>',
data() {
return {
foo: '',
}
},
created(this: any) {
expect(this.$options.parent).toBeDefined()
expect(this.$options.test).toBeUndefined()
this.$options.test = this.id
expect(this.$options.test).toBe(this.id)
},
}
new Vue({
template: `<div><Comp id="1"/><Comp id="2"/></div>`,
components: { Comp },
}).$mount()
})
test('other private APIs', () => {
new Vue({
created() {
expect(this.$createElement).toBeTruthy()
},
})
new Vue({
compatConfig: {
PRIVATE_APIS: false,
},
created() {
expect(this.$createElement).toBeUndefined()
},
})
})