VueJs 2.0 phát ra sự kiện từ đứa con lớn đến thành phần cha mẹ của nó


89

Có vẻ như Vue.js 2.0 không phát ra các sự kiện từ một đứa trẻ lớn đến thành phần cha mẹ của nó.

Vue.component('parent', {
  template: '<div>I am the parent - {{ action }} <child @eventtriggered="performAction"></child></div>',
  data(){
    return {
      action: 'No action'
    }
  },
  methods: {
    performAction() { this.action = 'actionDone' }
  }
})

Vue.component('child', {
  template: '<div>I am the child <grand-child></grand-child></div>'
})

Vue.component('grand-child', {
  template: '<div>I am the grand-child <button @click="doEvent">Do Event</button></div>',
  methods: {
    doEvent() { this.$emit('eventtriggered') }
  }
})

new Vue({
  el: '#app'
})

JsFiddle này giải quyết vấn đề https://jsfiddle.net/y5dvkqbd/4/ , nhưng bằng cách tạo hai sự kiện:

  • Một từ trẻ lớn đến thành phần trung lưu
  • Sau đó, lại phát ra từ thành phần giữa đến thành phần chính

Thêm sự kiện giữa này có vẻ lặp đi lặp lại và không cần thiết. Có cách nào để phát trực tiếp cho ông bà mà tôi không biết không?

Câu trả lời:


66

Vue 2.4 đã giới thiệu một cách để dễ dàng chuyển các sự kiện lên hệ thống phân cấp bằng cách sử dụng vm.$listeners

Từ https://vuejs.org/v2/api/#vm-listaries :

Chứa trình v-onnghe sự kiện phạm vi cha (không có .nativebổ ngữ). Điều này có thể được truyền cho một thành phần bên trong thông qua v-on="$listeners"- hữu ích khi tạo các thành phần trình bao bọc trong suốt.

Xem đoạn mã dưới đây sử dụng v-on="$listeners"trong grand-childthành phần trong childmẫu:

Vue.component('parent', {
  template:
    '<div>' +
      '<p>I am the parent. The value is {{displayValue}}.</p>' +
      '<child @toggle-value="toggleValue"></child>' +
    '</div>',
  data() {
    return {
      value: false
    }
  },
  methods: {
    toggleValue() { this.value = !this.value }
  },
  computed: {
    displayValue() {
      return (this.value ? "ON" : "OFF")
    }
  }
})

Vue.component('child', {
  template:
    '<div class="child">' +
      '<p>I am the child. I\'m just a wrapper providing some UI.</p>' +
      '<grand-child v-on="$listeners"></grand-child>' +
    '</div>'
})

Vue.component('grand-child', {
  template:
    '<div class="child">' +
      '<p>I am the grand-child: ' +
        '<button @click="emitToggleEvent">Toggle the value</button>' +
      '</p>' +
    '</div>',
  methods: {
    emitToggleEvent() { this.$emit('toggle-value') }
  }
})

new Vue({
  el: '#app'
})
.child {
  padding: 10px;
  border: 1px solid #ddd;
  background: #f0f0f0
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <parent></parent>
</div>


Tôi thấy đây là cách sạch nhất mà không cần giới thiệu vuex / bus / root. Điều này sẽ chuyển tất cả hàm phát lên trên. Bạn vẫn có thể khai thác phần phát ra của trẻ em bằng cách đặt @ someEmit = "someEmitHandler" trước v-on = "$
listening

39

Cộng đồng Vue thường ủng hộ việc sử dụng Vuex để giải quyết loại vấn đề này. Các thay đổi được thực hiện đối với trạng thái Vuex và đại diện DOM chỉ bắt nguồn từ đó, loại bỏ sự cần thiết của các sự kiện trong nhiều trường hợp.

Ngoại trừ điều đó, phát lại có lẽ sẽ là lựa chọn tốt nhất tiếp theo và cuối cùng bạn có thể chọn sử dụng xe buýt sự kiện như được nêu chi tiết trong câu trả lời được bình chọn cao khác cho câu hỏi này.

Câu trả lời dưới đây là câu trả lời ban đầu của tôi cho câu hỏi này và không phải là cách tiếp cận mà tôi sẽ thực hiện bây giờ, có nhiều kinh nghiệm hơn với Vue.


Đây là trường hợp mà tôi có thể không đồng ý với lựa chọn thiết kế của Vue và sử dụng DOM.

Trong grand-child,

methods: {
    doEvent() { 
        try {
            this.$el.dispatchEvent(new Event("eventtriggered"));
        } catch (e) {
            // handle IE not supporting Event constructor
            var evt = document.createEvent("Event");
            evt.initEvent("eventtriggered", true, false);
            this.$el.dispatchEvent(evt);
        }
    }
}

và trong parent,

mounted(){
    this.$el.addEventListener("eventtriggered", () => this.performAction())
}

Nếu không, có, bạn phải phát lại, hoặc sử dụng xe buýt.

Lưu ý: Tôi đã thêm mã trong phương thức doEvent để xử lý IE; mã đó có thể được trích xuất theo cách có thể sử dụng lại.


Điều này hoạt động khác nhau đối với IE? không biết đã có sự khác biệt với trình duyệt vue ...
BassMHL

@BassemLhm Vue ổn với IE. Vấn đề với IE không phải là Vue, đây là một giải pháp DOM và bạn không thể tạo Event () mới trong IE. Bạn phải document.createEvent (). Tôi có thể thêm hỗ trợ IE nếu cần.
Bert

Không hợp lý khi cài đặt vuex chỉ cho một trường hợp đơn giản.
Adam Orlov

@AdamOrlov Tôi đồng ý với bạn.
Bert

Một giải pháp đơn giản hơn: stackoverflow.com/a/55650245/841591
digout

28

CÂU TRẢ LỜI MỚI (Bản cập nhật tháng 11 năm 2018)

Tôi phát hiện ra rằng chúng tôi thực sự có thể làm điều này bằng cách tận dụng thuộc $parenttính trong thành phần con lớn:

this.$parent.$emit("submit", {somekey: somevalue})

Gọn gàng và đơn giản hơn nhiều.


16
Lưu ý rằng điều này chỉ hoạt động nếu mối quan hệ là con -> ông bà. Điều này không hiệu quả nếu đứa trẻ có thể được lồng các cấp độ tùy ý sâu.
Qtax

Xem câu trả lời của tôi stackoverflow.com/a/55650245/841591 để phản hồi nhận xét từ @Qtax
digout

7
Bạn không muốn những điều như vậy xảy ra trong dự án lớn của mình. Bạn đặt 'đứa trẻ' vào một transitionhoặc bất kỳ thành phần trình bao bọc nào khác và nó sẽ bị vỡ, để lại cho bạn một dấu hỏi lớn trong đầu.
Adam Orlov

@AdamOrlov Tôi đồng ý, đây là cách làm không tốt. Vui lòng xử lý các sự kiện như thế này bằng cách sử dụng cửa hàng Vuex.
Fabian von Ellerts

Điều này đơn giản hơn: stackoverflow.com/a/55650245/841591
digout

26

Vâng, bạn đúng là các sự kiện chỉ đi từ con đến cha. Họ không đi xa hơn, ví dụ như từ trẻ em đến ông bà.

Tài liệu Vue (ngắn gọn) giải quyết tình huống này trong phần Giao tiếp không phải của cha mẹ-con cái .

Ý tưởng chung là trong thành phần ông bà, bạn tạo một Vuethành phần trống được truyền từ ông bà xuống con cháu thông qua đạo cụ. Sau đó ông bà lắng nghe các sự kiện và cháu phát ra các sự kiện trên "xe buýt sự kiện" đó.

Một số ứng dụng sử dụng bus sự kiện toàn cục thay vì bus sự kiện từng thành phần. Sử dụng bus sự kiện toàn cầu có nghĩa là bạn sẽ cần phải có tên sự kiện hoặc không gian tên duy nhất để các sự kiện không xung đột giữa các thành phần khác nhau.

Đây là một ví dụ về cách triển khai một bus sự kiện toàn cầu đơn giản .


16

Một giải pháp khác sẽ được bật / phát tại nút gốc :

Sử dụng vm.$root.$emittrong grand-child , sau đó sử dụng vm.$root.$onở tổ tiên (hoặc bất kỳ nơi nào bạn muốn).

Đã cập nhật : đôi khi bạn muốn tắt trình nghe trong một số tình huống cụ thể, hãy sử dụng vm. $ Off (ví dụ: vm.$root.off('event-name')bên trong vòng đời hook = beforeDestroy ).

Vue.component('parent', {
  template: '<div><button @click="toggleEventListener()">Listener is {{eventEnable ? "On" : "Off"}}</button>I am the parent - {{ action }} <child @eventtriggered="performAction"></child></div>',
  data(){
    return {
      action: 1,
      eventEnable: false
    }
  },
  created: function () {
    this.addEventListener()
  },
  beforeDestroy: function () {
    this.removeEventListener()
  },
  methods: {
    performAction() { this.action += 1 },
    toggleEventListener: function () {
      if (this.eventEnable) {
        this.removeEventListener()
      } else {
        this.addEventListener()
      }
    },
    addEventListener: function () {
      this.$root.$on('eventtriggered1', () => {
        this.performAction()
      })
      this.eventEnable = true
    },
    removeEventListener: function () {
      this.$root.$off('eventtriggered1')
      this.eventEnable = false
    }
  }
})

Vue.component('child', {
  template: '<div>I am the child <grand-child @eventtriggered="doEvent"></grand-child></div>',
  methods: {
    doEvent() { 
    	//this.$emit('eventtriggered') 
    }
  }
})

Vue.component('grand-child', {
  template: '<div>I am the grand-child <button @click="doEvent">Emit Event</button></div>',
  methods: {
    doEvent() { this.$root.$emit('eventtriggered1') }
  }
})

new Vue({
  el: '#app'
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>

<div id="app">
  <parent></parent>
</div>


Giải pháp thanh lịch nhất cho tôi.
Ovilia

Giải pháp tốt! Điều này hoạt động tốt đối với tôi trong một ứng dụng chủ yếu phía máy chủ (Laravel) với một vài thành phần Vue (tôi không muốn giới thiệu một lớp Vuex). Vấn đề cụ thể của tôi là thành phần nút tùy chỉnh đã kích hoạt phương thức Buefy với thành phần biểu mẫu. Sau khi biểu mẫu được gửi, tôi muốn vô hiệu hóa nút, nhưng các sự kiện tùy chỉnh được phát ra từ biểu mẫu sẽ không đến được nút vì Phương thức là cha trực tiếp của biểu mẫu.
Richard

Điều này không phá hủy người nghe khi hủy. Ổn chứ? Sẽ thật tuyệt nếu bạn có thể thêm logic đó, vì vậy tôi cũng biết cách làm điều đó.
mesqueeb

14

Nếu bạn muốn linh hoạt và đơn giản truyền phát một sự kiện cho tất cả cha mẹ và cha mẹ của họ một cách đệ quy lên tới thư mục gốc, bạn có thể làm như sau:

let vm = this.$parent

while(vm) {
    vm.$emit('submit')
    vm = vm.$parent
}

1
Đây là câu trả lời đúng. Tôi đã tạo một hàm tiện ích để thực hiện việc đó ở nhiều nơi trong mã của mình: tuyên truyềnEvent (component, eventName, value) {while (component) {component. $ Nhũ (eventName, value); component = thành phần. $ cha; }}
ccleve

2
Đây là một câu trả lời thanh lịch, rất đơn giản để sử dụng và dễ đọc. Và nó không bị hỏng bằng cách thêm các cấp gói bổ sung.
Dean

4

Đây là trường hợp duy nhất khi tôi sử dụng xe buýt sự kiện !! Để truyền dữ liệu từ con lồng nhau sâu, không trực tiếp cha mẹ, giao tiếp.

Đầu tiên : Tạo một tệp js (tôi đặt tên là eventbus.js) với nội dung sau:

import Vue from 'vue'    
Vue.prototype.$event = new Vue()

Thứ hai : Trong thành phần con của bạn phát ra một sự kiện:

this.$event.$emit('event_name', 'data to pass')

Thứ ba : Trong cha mẹ lắng nghe sự kiện đó:

this.$event.$on('event_name', (data) => {
  console.log(data)
})

Lưu ý: Nếu bạn không muốn sự kiện đó nữa, vui lòng hủy đăng ký:

this.$event.$off('event_name')

THÔNG TIN: Không cần đọc ý kiến ​​cá nhân bên dưới

Tôi không thích sử dụng vuex cho giao tiếp giữa con cháu với ông bà (Hoặc cấp độ giao tiếp tương tự).

Trong vue.js để chuyển dữ liệu từ ông bà sang con cháu, bạn có thể sử dụng cung cấp / tiêm . Nhưng không có cái gì đó tương tự cho điều ngược lại. (cháu với ông bà) Vì vậy, tôi sử dụng xe buýt sự kiện bất cứ khi nào tôi phải thực hiện loại giao tiếp đó.


2

Tôi đã tạo một bản mixin ngắn dựa trên câu trả lời @digout. Bạn muốn đặt nó, trước khi khởi tạo thể hiện Vue của bạn (Vue mới ...) để sử dụng nó trên toàn cầu trong dự án. Bạn có thể sử dụng nó tương tự như sự kiện bình thường.

Vue.mixin({
  methods: {
    $propagatedEmit: function (event, payload) {
      let vm = this.$parent;
      while (vm) {
        vm.$emit(event, payload);
        vm = vm.$parent;
      }
    }
  }
})

giải pháp này là những gì tôi đã sử dụng cho việc triển khai của mình nhưng tôi đã thêm một tham số bổ sung targetRefđể dừng việc truyền trên thành phần mà bạn đang nhắm mục tiêu. Sau đó, whileđiều kiện sẽ bao gồm && vm.$refs[targetRef]- bạn cũng cần phải bao gồm refthuộc tính đó trên thành phần được nhắm mục tiêu Trong trường hợp sử dụng của tôi, tôi không cần phải đào tất cả các cách để root, lưu một vài sự kiện khỏi kích hoạt và có thể là một vài nano giây quý giá time
mcgraw

2

VueJS 2 thành phần có một $parent tính chứa thành phần mẹ của chúng.

Thành phần chính đó cũng bao gồm $parent tài sản .

Sau đó, truy cập thành phần "grandparent", vấn đề là truy cập thành phần "parent's parent":

this.$parent["$parent"].$emit("myevent", { data: 123 });

Dù sao, điều này hơi khó và tôi khuyên bạn nên sử dụng trình quản lý nhà nước toàn cầu như Vuex hoặc các công cụ tương tự, như những người phản hồi khác đã nói.


1

Làm xáo trộn câu trả lời của @kubaklam và @ digout, đây là những gì tôi sử dụng để tránh phát ra trên mọi thành phần cha mẹ giữa cháu và ông bà (có thể ở xa):

{
  methods: {
    tunnelEmit (event, ...payload) {
      let vm = this
      while (vm && !vm.$listeners[event]) {
        vm = vm.$parent
      }
      if (!vm) return console.error(`no target listener for event "${event}"`)
      vm.$emit(event, ...payload)
    }
  }
}

Khi xây dựng một thành phần với những đứa con xa, nơi bạn không muốn nhiều / bất kỳ thành phần nào bị ràng buộc với cửa hàng, nhưng muốn thành phần gốc hoạt động như một cửa hàng / nguồn chân lý, điều này hoạt động khá tốt. Điều này tương tự với triết lý giảm dữ liệu của Ember. Nhược điểm là nếu bạn muốn lắng nghe sự kiện đó trên mọi phụ huynh ở giữa, thì điều này sẽ không hoạt động. Nhưng sau đó bạn có thể sử dụng $ propogateEmit như trong câu trả lời ở trên của @kubaklam.

Chỉnh sửa: vm ban đầu nên được đặt thành thành phần, và không phải là cha của thành phần. Tức let vm = thislà khônglet vm = this.$parent


0

Tôi thực sự tìm hiểu cách xử lý vấn đề này bằng cách tạo một lớp liên kết với cửa sổ và đơn giản hóa thiết lập phát / nghe để hoạt động mọi lúc mọi nơi trong ứng dụng Vue.

window.Event = new class {

    constructor() {
        this.vue = new Vue();
    }

    fire(event, data = null) {
        this.vue.$emit(event, data);
    }

    listen() {
        this.vue.$on(event, callback);  
    }

}

Bây giờ bạn có thể chỉ cần kích hoạt / phát sóng / bất cứ thứ gì từ bất kỳ đâu bằng cách gọi:

Event.fire('do-the-thing');

... và bạn có thể nghe từ cha mẹ, ông bà, bất cứ điều gì bạn muốn bằng cách gọi:

Event.listen('do-the-thing', () => {
    alert('Doing the thing!');
});

1
Tôi thực sự khuyên bạn không nên gắn các thuộc tính ngẫu nhiên vào đối tượng window, vì rất dễ ghi đè các thuộc tính hiện có hoặc xung đột với các thư viện bên thứ 3 hiện có. Thay vào đó, bất cứ ai sử dụng Vue để giải quyết vấn đề này để thay thế nên sử dụng câu trả lời Roli @roli của
HMilbradt

1
Tôi không chắc mình hoàn toàn hiểu hoặc đồng ý với mối quan tâm này. Liên kết với nguyên mẫu là một cách tiếp cận tốt nhưng ràng buộc với cửa sổ cũng giống như, nếu không muốn nói là phổ biến hơn và có lẽ là cách xử lý tiêu chuẩn hơn. Bạn đặt tên cho tài sản, vì vậy thật đơn giản để tránh xung đột đặt tên. medium.com/@amitavroy7/… stackoverflow.com/questions/15008464/… Đây cũng là giải pháp được đề xuất mà Jeff Way sử dụng trên Laracasts. laracasts.com/series/learn-vue-2-step-by-step/episodes/13
fylzero
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.