Browse Source

avoid duplicate lifecycle hooks during constructor resolution

Evan You 9 years ago
parent
commit
7fa8fa76fe
2 changed files with 41 additions and 14 deletions
  1. 25 8
      src/core/instance/init.js
  2. 16 6
      test/unit/features/global-api/mixin.spec.js

+ 25 - 8
src/core/instance/init.js

@@ -86,7 +86,7 @@ function initInternalComponent (vm: Component, options: InternalComponentOptions
 export function resolveConstructorOptions (Ctor: Class<Component>) {
   let options = Ctor.options
   if (Ctor.super) {
-    const superOptions = Ctor.super.options
+    const superOptions = resolveConstructorOptions(Ctor.super)
     const cachedSuperOptions = Ctor.superOptions
     if (superOptions !== cachedSuperOptions) {
       // super option changed,
@@ -108,14 +108,31 @@ export function resolveConstructorOptions (Ctor: Class<Component>) {
 }
 
 function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
-  let res
-  const options = Ctor.options
+  let modified
+  const latest = Ctor.options
   const sealed = Ctor.sealedOptions
-  for (const key in options) {
-    if (sealed[key] !== options[key]) {
-      if (!res) res = {}
-      res[key] = options[key]
+  for (const key in latest) {
+    if (latest[key] !== sealed[key]) {
+      if (!modified) modified = {}
+      modified[key] = dedupe(latest[key], sealed[key])
     }
   }
-  return res
+  return modified
+}
+
+function dedupe (latest, 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]
+    for (let i = 0; i < latest.length; i++) {
+      if (sealed.indexOf(latest[i]) < 0) {
+        res.push(latest[i])
+      }
+    }
+    return res
+  } else {
+    return latest
+  }
 }

+ 16 - 6
test/unit/features/global-api/mixin.spec.js

@@ -87,7 +87,12 @@ describe('Global API: mixin', () => {
 
   // #4976
   it('should not drop late-attached custom options on existing constructors', () => {
-    const Test = Vue.extend({})
+    const baseSpy = jasmine.createSpy('base')
+    const Base = Vue.extend({
+      beforeCreate: baseSpy
+    })
+
+    const Test = Base.extend({})
 
     // Inject options later
     // vue-loader and vue-hot-reload-api are doing like this
@@ -95,23 +100,28 @@ describe('Global API: mixin', () => {
       $style: () => 123
     }
 
-    const spy = jasmine.createSpy('mixin')
-    Test.options.beforeCreate = [spy]
+    const spy = jasmine.createSpy('late attached')
+    Test.options.beforeCreate = Test.options.beforeCreate.concat(spy)
 
     // Update super constructor's options
-    Vue.mixin({})
+    const mixinSpy = jasmine.createSpy('mixin')
+    Vue.mixin({
+      beforeCreate: mixinSpy
+    })
 
     // mount the component
     const vm = new Test({
       template: '<div>{{ $style }}</div>'
     }).$mount()
 
-    expect(spy).toHaveBeenCalled()
+    expect(spy.calls.count()).toBe(1)
+    expect(baseSpy.calls.count()).toBe(1)
+    expect(mixinSpy.calls.count()).toBe(1)
     expect(vm.$el.textContent).toBe('123')
     expect(vm.$style).toBe(123)
 
     // Should not be dropped
     expect(Test.options.computed.$style()).toBe(123)
-    expect(Test.options.beforeCreate).toEqual([spy])
+    expect(Test.options.beforeCreate).toEqual([mixinSpy, baseSpy, spy])
   })
 })