Browse Source

fix(core): dedupe lifecycle hooks during options merge

The fix landed in #9199 causes further extended constructors used as
mixins to drop options from its inheritance chain, so a different fix
is needed.
dev
Evan You 6 years ago
parent
commit
edf7df0c83
  1. 22
      src/core/instance/init.js
  2. 17
      src/core/util/options.js
  3. 27
      test/unit/features/global-api/mixin.spec.js
  4. 38
      test/unit/features/options/mixins.spec.js

22
src/core/instance/init.js

@ -117,32 +117,12 @@ export function resolveConstructorOptions (Ctor: Class<Component>) {
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
let modified
const latest = Ctor.options
const extended = Ctor.extendOptions
const sealed = Ctor.sealedOptions
for (const key in latest) {
if (latest[key] !== sealed[key]) {
if (!modified) modified = {}
modified[key] = dedupe(latest[key], extended[key], sealed[key])
modified[key] = latest[key]
}
}
return modified
}
function dedupe (latest, extended, sealed) {
// compare latest and sealed to ensure lifecycle hooks won't be duplicated
// between merges
if (Array.isArray(latest)) {
const res = []
sealed = Array.isArray(sealed) ? sealed : [sealed]
extended = Array.isArray(extended) ? extended : [extended]
for (let i = 0; i < latest.length; i++) {
// push original options and not sealed options to exclude duplicated options
if (extended.indexOf(latest[i]) >= 0 || sealed.indexOf(latest[i]) < 0) {
res.push(latest[i])
}
}
return res
} else {
return latest
}
}

17
src/core/util/options.js

@ -147,13 +147,26 @@ function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res)
: res
}
function dedupeHooks (hooks) {
const res = []
for (let i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i])
}
}
return res
}
LIFECYCLE_HOOKS.forEach(hook => {
@ -382,7 +395,7 @@ export function mergeOptions (
}
if (typeof child === 'function') {
child = child.extendOptions
child = child.options
}
normalizeProps(child, vm)

27
test/unit/features/global-api/mixin.spec.js

@ -167,4 +167,31 @@ describe('Global API: mixin', () => {
it('chain call', () => {
expect(Vue.mixin({})).toBe(Vue)
})
// #9198
it('should not mix global mixin lifecycle hook twice', () => {
const spy = jasmine.createSpy('global mixed in lifecycle hook')
Vue.mixin({
created: spy
})
const mixin1 = Vue.extend({
methods: {
a() {}
}
})
const mixin2 = Vue.extend({
mixins: [mixin1]
})
const Child = Vue.extend({
mixins: [mixin2],
})
const vm = new Child()
expect(typeof vm.$options.methods.a).toBe('function')
expect(spy.calls.count()).toBe(1)
})
})

38
test/unit/features/options/mixins.spec.js

@ -110,31 +110,35 @@ describe('Options mixins', () => {
expect(vm.$options.directives.c).toBeDefined()
})
it('should not mix global mixined lifecycle hook twice', () => {
const spy = jasmine.createSpy('global mixed in lifecycle hook')
Vue.mixin({
created() {
spy()
}
})
it('should accept further extended constructors as mixins', () => {
const spy1 = jasmine.createSpy('mixinA')
const spy2 = jasmine.createSpy('mixinB')
const mixin1 = Vue.extend({
const mixinA = Vue.extend({
created: spy1,
directives: {
c: {}
},
methods: {
a() {}
a: function () {}
}
})
const mixin2 = Vue.extend({
mixins: [mixin1],
const mixinB = mixinA.extend({
created: spy2
})
const Child = Vue.extend({
mixins: [mixin2],
const vm = new Vue({
mixins: [mixinB],
methods: {
b: function () {}
}
})
const vm = new Child()
expect(typeof vm.$options.methods.a).toBe('function')
expect(spy.calls.count()).toBe(1)
expect(spy1).toHaveBeenCalledTimes(1)
expect(spy2).toHaveBeenCalledTimes(1)
expect(vm.a).toBeDefined()
expect(vm.b).toBeDefined()
expect(vm.$options.directives.c).toBeDefined()
})
})

Loading…
Cancel
Save