Просмотр исходного кода

fix event propagation handling in immediate inline component handlers

Evan You 10 лет назад
Родитель
Сommit
5d2c76f2bb
3 измененных файлов с 72 добавлено и 23 удалено
  1. 40 10
      src/instance/api/events.js
  2. 1 0
      src/instance/internal/events.js
  3. 31 13
      test/unit/specs/api/events_spec.js

+ 40 - 10
src/instance/api/events.js

@@ -85,21 +85,36 @@ export default function (Vue) {
   /**
    * Trigger an event on self.
    *
-   * @param {String} event
+   * @param {String|Object} event
    * @return {Boolean} shouldPropagate
    */
 
   Vue.prototype.$emit = function (event) {
+    var isSource = typeof event === 'string'
+    event = isSource
+      ? event
+      : event.name
     var cbs = this._events[event]
-    var shouldPropagate = !cbs
+    var shouldPropagate = isSource || !cbs
     if (cbs) {
       cbs = cbs.length > 1
         ? toArray(cbs)
         : cbs
+      // this is a somewhat hacky solution to the question raised
+      // in #2102: for an inline component listener like <comp @test="doThis">,
+      // the propagation handling is somewhat broken. Therefore we
+      // need to treat these inline callbacks differently.
+      var hasParentCbs = isSource && cbs.some(function (cb) {
+        return cb._fromParent
+      })
+      if (hasParentCbs) {
+        shouldPropagate = false
+      }
       var args = toArray(arguments, 1)
       for (var i = 0, l = cbs.length; i < l; i++) {
-        var res = cbs[i].apply(this, args)
-        if (res === true) {
+        var cb = cbs[i]
+        var res = cb.apply(this, args)
+        if (res === true && (!hasParentCbs || cb._fromParent)) {
           shouldPropagate = true
         }
       }
@@ -110,20 +125,30 @@ export default function (Vue) {
   /**
    * Recursively broadcast an event to all children instances.
    *
-   * @param {String} event
+   * @param {String|Object} event
    * @param {...*} additional arguments
    */
 
   Vue.prototype.$broadcast = function (event) {
+    var isSource = typeof event === 'string'
+    event = isSource
+      ? event
+      : event.name
     // if no child has registered for this event,
     // then there's no need to broadcast.
     if (!this._eventsCount[event]) return
     var children = this.$children
+    var args = toArray(arguments)
+    if (isSource) {
+      // use object event to indicate non-source emit
+      // on children
+      args[0] = { name: event, source: this }
+    }
     for (var i = 0, l = children.length; i < l; i++) {
       var child = children[i]
-      var shouldPropagate = child.$emit.apply(child, arguments)
+      var shouldPropagate = child.$emit.apply(child, args)
       if (shouldPropagate) {
-        child.$broadcast.apply(child, arguments)
+        child.$broadcast.apply(child, args)
       }
     }
     return this
@@ -136,11 +161,16 @@ export default function (Vue) {
    * @param {...*} additional arguments
    */
 
-  Vue.prototype.$dispatch = function () {
-    this.$emit.apply(this, arguments)
+  Vue.prototype.$dispatch = function (event) {
+    var shouldPropagate = this.$emit.apply(this, arguments)
+    if (!shouldPropagate) return
     var parent = this.$parent
+    var args = toArray(arguments)
+    // use object event to indicate non-source emit
+    // on parents
+    args[0] = { name: event, source: this }
     while (parent) {
-      var shouldPropagate = parent.$emit.apply(parent, arguments)
+      shouldPropagate = parent.$emit.apply(parent, args)
       parent = shouldPropagate
         ? parent.$parent
         : null

+ 1 - 0
src/instance/internal/events.js

@@ -38,6 +38,7 @@ export default function (Vue) {
       if (eventRE.test(name)) {
         name = name.replace(eventRE, '')
         handler = (vm._scope || vm._context).$eval(attrs[i].value, true)
+        handler._fromParent = true
         vm.$on(name.replace(eventRE), handler)
       }
     }

+ 31 - 13
test/unit/specs/api/events_spec.js

@@ -165,7 +165,7 @@ describe('Events API', function () {
       },
       components: {
         child1: {
-          template: '<child2 @test="onTest()" v-ref:child></child2>',
+          template: '<child2 @test="onTest" v-ref:child></child2>',
           methods: {
             onTest: function () {
               spy()
@@ -182,9 +182,13 @@ describe('Events API', function () {
               },
               components: {
                 child3: {
-                  template: '<child4 v-ref:child></child4>',
-                  // `v-on` on component will be treat as its inner handler
-                  // so propagation cancelling is ignored on `<child4 @test="handler">`
+                  template: '<child4 @test="onTest" v-ref:child></child4>',
+                  methods: {
+                    onTest: function () {
+                      spy()
+                      return true
+                    }
+                  },
                   components: {
                     child4: {}
                   }
@@ -201,18 +205,32 @@ describe('Events API', function () {
       .$refs.child // child3
       .$refs.child // child4
       .$dispatch('test')
-    expect(spy.calls.count()).toBe(2)
+    expect(spy.calls.count()).toBe(3)
   })
 
-  it('$dispatch cancel', function () {
-    var child = new Vue({ parent: vm })
-    var child2 = new Vue({ parent: child })
-    child.$on('test', function () {
-      return false
+  it('$dispatch forward on immediate inline component handler', function () {
+    var shouldPropagate = true
+    var parent = new Vue({
+      el: document.createElement('div'),
+      template: '<child @test="onTest" v-ref:child></child>',
+      events: {
+        test: spy
+      },
+      methods: {
+        onTest: function () {
+          spy()
+          return shouldPropagate
+        }
+      },
+      components: {
+        child: {}
+      }
     })
-    vm.$on('test', spy)
-    child2.$dispatch('test')
-    expect(spy).not.toHaveBeenCalled()
+    parent.$refs.child.$dispatch('test')
+    expect(spy.calls.count()).toBe(2)
+    shouldPropagate = false
+    parent.$refs.child.$dispatch('test')
+    expect(spy.calls.count()).toBe(3)
   })
 
 })