Skip to content

Commit e2ca67b

Browse files
committed
fix(runtime-core): align option merge behavior with Vue 2
fix #3566, #2791
1 parent 1e35a86 commit e2ca67b

File tree

9 files changed

+435
-371
lines changed

9 files changed

+435
-371
lines changed

Diff for: packages/runtime-core/__tests__/apiOptions.spec.ts

+182
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,188 @@ describe('api: options', () => {
10661066
)
10671067
})
10681068

1069+
describe('options merge strategies', () => {
1070+
test('this.$options.data', () => {
1071+
const mixin = {
1072+
data() {
1073+
return { foo: 1, bar: 2 }
1074+
}
1075+
}
1076+
createApp({
1077+
mixins: [mixin],
1078+
data() {
1079+
return {
1080+
foo: 3,
1081+
baz: 4
1082+
}
1083+
},
1084+
created() {
1085+
expect(this.$options.data).toBeInstanceOf(Function)
1086+
expect(this.$options.data()).toEqual({
1087+
foo: 3,
1088+
bar: 2,
1089+
baz: 4
1090+
})
1091+
},
1092+
render: () => null
1093+
}).mount(nodeOps.createElement('div'))
1094+
})
1095+
1096+
test('this.$options.inject', () => {
1097+
const mixin = {
1098+
inject: ['a']
1099+
}
1100+
const app = createApp({
1101+
mixins: [mixin],
1102+
inject: { b: 'b', c: { from: 'd' } },
1103+
created() {
1104+
expect(this.$options.inject.a).toEqual('a')
1105+
expect(this.$options.inject.b).toEqual('b')
1106+
expect(this.$options.inject.c).toEqual({ from: 'd' })
1107+
expect(this.a).toBe(1)
1108+
expect(this.b).toBe(2)
1109+
expect(this.c).toBe(3)
1110+
},
1111+
render: () => null
1112+
})
1113+
1114+
app.provide('a', 1)
1115+
app.provide('b', 2)
1116+
app.provide('d', 3)
1117+
app.mount(nodeOps.createElement('div'))
1118+
})
1119+
1120+
test('this.$options.provide', () => {
1121+
const mixin = {
1122+
provide: {
1123+
a: 1
1124+
}
1125+
}
1126+
createApp({
1127+
mixins: [mixin],
1128+
provide() {
1129+
return {
1130+
b: 2
1131+
}
1132+
},
1133+
created() {
1134+
expect(this.$options.provide).toBeInstanceOf(Function)
1135+
expect(this.$options.provide()).toEqual({ a: 1, b: 2 })
1136+
},
1137+
render: () => null
1138+
}).mount(nodeOps.createElement('div'))
1139+
})
1140+
1141+
test('this.$options[lifecycle-name]', () => {
1142+
const mixin = {
1143+
mounted() {}
1144+
}
1145+
createApp({
1146+
mixins: [mixin],
1147+
mounted() {},
1148+
created() {
1149+
expect(this.$options.mounted).toBeInstanceOf(Array)
1150+
expect(this.$options.mounted.length).toBe(2)
1151+
},
1152+
render: () => null
1153+
}).mount(nodeOps.createElement('div'))
1154+
})
1155+
1156+
test('this.$options[asset-name]', () => {
1157+
const mixin = {
1158+
components: {
1159+
a: {}
1160+
},
1161+
directives: {
1162+
d1: {}
1163+
}
1164+
}
1165+
createApp({
1166+
mixins: [mixin],
1167+
components: {
1168+
b: {}
1169+
},
1170+
directives: {
1171+
d2: {}
1172+
},
1173+
created() {
1174+
expect('a' in this.$options.components).toBe(true)
1175+
expect('b' in this.$options.components).toBe(true)
1176+
expect('d1' in this.$options.directives).toBe(true)
1177+
expect('d2' in this.$options.directives).toBe(true)
1178+
},
1179+
render: () => null
1180+
}).mount(nodeOps.createElement('div'))
1181+
})
1182+
1183+
test('this.$options.methods', () => {
1184+
const mixin = {
1185+
methods: {
1186+
fn1() {}
1187+
}
1188+
}
1189+
createApp({
1190+
mixins: [mixin],
1191+
methods: {
1192+
fn2() {}
1193+
},
1194+
created() {
1195+
expect(this.$options.methods.fn1).toBeInstanceOf(Function)
1196+
expect(this.$options.methods.fn2).toBeInstanceOf(Function)
1197+
},
1198+
render: () => null
1199+
}).mount(nodeOps.createElement('div'))
1200+
})
1201+
1202+
test('this.$options.computed', () => {
1203+
const mixin = {
1204+
computed: {
1205+
c1() {}
1206+
}
1207+
}
1208+
createApp({
1209+
mixins: [mixin],
1210+
computed: {
1211+
c2() {}
1212+
},
1213+
created() {
1214+
expect(this.$options.computed.c1).toBeInstanceOf(Function)
1215+
expect(this.$options.computed.c2).toBeInstanceOf(Function)
1216+
},
1217+
render: () => null
1218+
}).mount(nodeOps.createElement('div'))
1219+
})
1220+
1221+
// #2791
1222+
test('modify $options in the beforeCreate hook', async () => {
1223+
const count = ref(0)
1224+
const mixin = {
1225+
data() {
1226+
return { foo: 1 }
1227+
},
1228+
beforeCreate(this: any) {
1229+
if (!this.$options.computed) {
1230+
this.$options.computed = {}
1231+
}
1232+
this.$options.computed.value = () => count.value
1233+
}
1234+
}
1235+
const root = nodeOps.createElement('div')
1236+
createApp({
1237+
mixins: [mixin],
1238+
render(this: any) {
1239+
return this.value
1240+
}
1241+
}).mount(root)
1242+
1243+
expect(serializeInner(root)).toBe('0')
1244+
1245+
count.value++
1246+
await nextTick()
1247+
expect(serializeInner(root)).toBe('1')
1248+
})
1249+
})
1250+
10691251
describe('warnings', () => {
10701252
test('Expected a function as watch handler', () => {
10711253
const Comp = {

Diff for: packages/runtime-core/src/apiCreateApp.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import {
44
validateComponentName,
55
Component
66
} from './component'
7-
import { ComponentOptions, RuntimeCompilerOptions } from './componentOptions'
7+
import {
8+
ComponentOptions,
9+
MergedComponentOptions,
10+
RuntimeCompilerOptions
11+
} from './componentOptions'
812
import { ComponentPublicInstance } from './componentPublicInstance'
913
import { Directive, validateDirectiveName } from './directives'
1014
import { RootRenderFunction } from './renderer'
@@ -98,7 +102,7 @@ export interface AppContext {
98102
* Each app instance has its own cache because app-level global mixins and
99103
* optionMergeStrategies can affect merge behavior.
100104
*/
101-
cache: WeakMap<ComponentOptions, ComponentOptions>
105+
cache: WeakMap<ComponentOptions, MergedComponentOptions>
102106
/**
103107
* Flag for de-optimizing props normalization
104108
* @internal

Diff for: packages/runtime-core/src/compat/compatConfig.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,10 @@ const seenConfigObjects = /*#__PURE__*/ new WeakSet<CompatConfig>()
531531
const warnedInvalidKeys: Record<string, boolean> = {}
532532

533533
// dev only
534-
export function validateCompatConfig(config: CompatConfig) {
534+
export function validateCompatConfig(
535+
config: CompatConfig,
536+
instance?: ComponentInternalInstance
537+
) {
535538
if (seenConfigObjects.has(config)) {
536539
return
537540
}
@@ -558,6 +561,14 @@ export function validateCompatConfig(config: CompatConfig) {
558561
warnedInvalidKeys[key] = true
559562
}
560563
}
564+
565+
if (instance && config[DeprecationTypes.OPTIONS_DATA_MERGE] != null) {
566+
warn(
567+
`Deprecation config "${
568+
DeprecationTypes.OPTIONS_DATA_MERGE
569+
}" can only be configured globally.`
570+
)
571+
}
561572
}
562573

563574
export function getCompatConfigForKey(

Diff for: packages/runtime-core/src/compat/data.ts

+4-27
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,16 @@
1-
import { isFunction, isPlainObject } from '@vue/shared'
2-
import { ComponentInternalInstance } from '../component'
3-
import { ComponentPublicInstance } from '../componentPublicInstance'
1+
import { isPlainObject } from '@vue/shared'
42
import { DeprecationTypes, warnDeprecation } from './compatConfig'
53

6-
export function deepMergeData(
7-
to: any,
8-
from: any,
9-
instance: ComponentInternalInstance
10-
) {
4+
export function deepMergeData(to: any, from: any) {
115
for (const key in from) {
126
const toVal = to[key]
137
const fromVal = from[key]
148
if (key in to && isPlainObject(toVal) && isPlainObject(fromVal)) {
15-
__DEV__ &&
16-
warnDeprecation(DeprecationTypes.OPTIONS_DATA_MERGE, instance, key)
17-
deepMergeData(toVal, fromVal, instance)
9+
__DEV__ && warnDeprecation(DeprecationTypes.OPTIONS_DATA_MERGE, null, key)
10+
deepMergeData(toVal, fromVal)
1811
} else {
1912
to[key] = fromVal
2013
}
2114
}
2215
return to
2316
}
24-
25-
export function mergeDataOption(to: any, from: any) {
26-
if (!from) {
27-
return to
28-
}
29-
if (!to) {
30-
return from
31-
}
32-
return function mergedDataFn(this: ComponentPublicInstance) {
33-
return deepMergeData(
34-
isFunction(to) ? to.call(this, this) : to,
35-
isFunction(from) ? from.call(this, this) : from,
36-
this.$
37-
)
38-
}
39-
}

Diff for: packages/runtime-core/src/compat/global.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ import {
3434
isRuntimeOnly,
3535
setupComponent
3636
} from '../component'
37-
import { RenderFunction, mergeOptions } from '../componentOptions'
37+
import {
38+
RenderFunction,
39+
mergeOptions,
40+
internalOptionMergeStrats
41+
} from '../componentOptions'
3842
import { ComponentPublicInstance } from '../componentPublicInstance'
3943
import { devtoolsInitApp, devtoolsUnmountApp } from '../devtools'
4044
import { Directive } from '../directives'
@@ -43,8 +47,7 @@ import { version } from '..'
4347
import {
4448
installLegacyConfigWarnings,
4549
installLegacyOptionMergeStrats,
46-
LegacyConfig,
47-
legacyOptionMergeStrats
50+
LegacyConfig
4851
} from './globalConfig'
4952
import { LegacyDirective } from './customDirective'
5053
import {
@@ -231,8 +234,7 @@ export function createCompatVue(
231234
mergeOptions(
232235
extend({}, SubVue.options),
233236
inlineOptions,
234-
null,
235-
legacyOptionMergeStrats as any
237+
internalOptionMergeStrats as any
236238
),
237239
SubVue
238240
)
@@ -257,8 +259,7 @@ export function createCompatVue(
257259
SubVue.options = mergeOptions(
258260
mergeBase,
259261
extendOptions,
260-
null,
261-
legacyOptionMergeStrats as any
262+
internalOptionMergeStrats as any
262263
)
263264

264265
SubVue.options._base = SubVue
@@ -305,8 +306,7 @@ export function createCompatVue(
305306
mergeOptions(
306307
parent,
307308
child,
308-
vm && vm.$,
309-
vm ? undefined : (legacyOptionMergeStrats as any)
309+
vm ? undefined : (internalOptionMergeStrats as any)
310310
),
311311
defineReactive
312312
}

0 commit comments

Comments
 (0)