Vue从入门到实战:监听子组件事件

来自CloudWiki
跳转至: 导航搜索

子组件的某些功能需要和父组件进行通信,应该如何实现呢 ?

在Vue.js中,这是通过自定义事件来实现的。子组件使用$emit()方法触发事件,父组件使用v-on指令监听子组件的自定义事件。

$emit()方法的语法形式如下:

 vm.$emit(eventName,[...args])

eventName是事件名,args是附件参数,如果子组件需要向父组件传递参数,就可以通过第二个参数来传。

子组件可以通过调用内建的 $emit 方法并传入事件名称来触发一个事件:

<button v-on:click="$emit('enlarge-text')">
  Enlarge text
</button>

父级组件可以像处理 native DOM 事件一样通过 v-on 监听子组件实例的任意事件:

<blog-post
  ...
  v-on:enlarge-text="postFontSize += 0.1"
></blog-post>

举例

设计两个组件来实现一个BBS项目

PostList负责整个帖子列表的渲染,

PostListItem负责单个帖子的渲染。“点赞”按钮在子组件中,为了向父组件通知点击事件,可以使用自定义事件的方式。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<script src="vue.js"></script>
	</head>
	<body>
		<div id="app">
			<post-list></post-list>
		</div>
	
		<script>
			// 父组件
			Vue.component('PostList', {
  			data() {
  				return {
  					posts: [
  						{id: 1, title: '《Servlet/JSP深入详解》怎么样', author: '张三', date: '2019-10-21 20:10:15', vote: 0},
  						{id: 2, title: '《VC++深入详解》观后感', author: '李四', date: '2019-10-10 09:15:11', vote: 0},
  						{id: 3, title: '《Vue.js无难事》怎么样', author: '王五', date: '2019-11-11 15:22:03', vote: 0}
  					]
  				}
  			},
  			methods: {
  				// 自定义事件vote的事件处理器方法
  				handleVote(id){
  					this.posts.map(item => {
  						item.id === id ? {...item, voite: ++item.vote} : item;
  					})
  				}
  			},
	      template: `
	      	<div>
	      		<ul>
	      			<PostListItem 
	      				v-for="post in posts" 
	      				:key="post.id" 
	      				:post="post" 
	      				@vote="handleVote(post.id)"/> <!--监听自定义事件-->
	      		</ul>
	      	</div>`
  		});
  		
  		// 子组件
			Vue.component('PostListItem', {
				methods: {
					handleVote(){
						// 触发自定义事件
						this.$emit('vote');
					}
				},
				props: ['post'],
	      template: `
	      	<li>
	      		<p>
	      			<span>标题:{{post.title}} | 发帖人:{{post.author}} | 发帖时间:{{post.date}} | 点赞数:{{post.vote}}</span>
	      			<button @click="handleVote">赞</button>
	      		</p>
	      	</li>`
  		});
  		
			new Vue({
			  el: '#app'
			})
		</script>
	</body>
</html>


将原生事件绑定到组件

为了解决这个问题,Vue 提供了一个 $listeners property,它是一个对象,里面包含了作用在这个组件上的所有监听器。例如:

{
  focus: function (event) { /* ... */ }
  input: function (value) { /* ... */ },
}

有了这个 $listeners property,你就可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。对于类似 <input> 的你希望它也可以配合 v-model 工作的组件来说,为这些监听器创建一个类似下述 inputListeners 的计算属性通常是非常有用的:

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  computed: {
    inputListeners: function () {
      var vm = this
      // `Object.assign` 将所有的对象合并为一个新对象
      return Object.assign({},
        // 我们从父级添加所有的监听器
        this.$listeners,
        // 然后我们添加自定义监听器,
        // 或覆写一些监听器的行为
        {
          // 这里确保组件配合 `v-model` 的工作
          input: function (event) {
            vm.$emit('input', event.target.value)
          }
        }
      )
    }
  },
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on="inputListeners"
      >
    </label>
  `
})

现在 <base-input> 组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的 <input> 元素一样使用了:所有跟它相同的 attribute 和监听器都可以工作,不必再使用 .native 监听器。

.sync修饰符

   2.3.0+ 新增

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件都没有明显的变更来源。

这也是为什么我们推荐以 update:myPropName 的模式触发事件取而代之。举个例子,在一个包含 title prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:

this.$emit('update:title', newTitle)

然后父组件可以监听那个事件并根据需要更新一个本地的数据 property。例如:

<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>

为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符:

<text-document v-bind:title.sync="doc.title"></text-document>

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。取而代之的是,你只能提供你想要绑定的 property 名,类似 v-model。

当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:

<text-document v-bind.sync="doc"></text-document>

这样会把 doc 对象中的每一个 property (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。

示例代码

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<script src="vue.js"></script>
	</head>
	<body>
		<div id="app">
			<span>计数器的值:{{counter}}</span>
			<!--
			<child 
				v-bind:val="counter" 
				v-on:update:val="counter = $event">
			</child>
			-->
			<child v-bind:val.sync="counter"></child>
				
		</div>
	
		<script>
			Vue.component('child', {
				data: function(){
					return {
						count: this.val
					}
				},
				props: {
					val: {
						type: Number,
						default: 0
					}
				},
				methods: {
					handleClick(){
						this.$emit('update:val', ++this.count);
					}
				},
	      template: 
	      	`<div>
	      		<span>计数:{{val}}</span>
	      		<button @click="handleClick">增加计数</button>
	      	</div>`
  		});
  
			new Vue({
			  el: '#app',
			  data: {
			  	counter: 0
			  }
			})
		</script>
	</body>
</html>