Vue从入门到实战:杂项

来自CloudWiki
跳转至: 导航搜索

组件通信的其他方式

前面介绍的组件通信的三种方式:

  • 父组件通过prop向子组件传递数据
  • 子组件通过自定义事件向父组件发起通知或进行数据传递
  • 子组件通过<slot>元素充当占位符,获取父组件分发的内容,也可以在子组件的<slot>元素上使用v-bind指令绑定一个插槽prop,向父组件提供数据。

除此之外,还有其他实现方式

访问根实例

在每一个new Vue实例的子组件中,都可以通过$root属性来访问根实例。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title></title>
		<script src="vue.js"></script>
	</head>
	<body>
		<div id="app">
			<parent></parent>
		</div>
	
		<script>
			Vue.component('parent', {
				template: '<child></child>'
			});
			Vue.component('child', {
			 	methods: {
			 		accessRoot(){
			 			console.log("单价:" + this.$root.price);
			 			console.log("总价:" + this.$root.totalPrice);
			 			console.log(this.$root.hello());
			 		}
			 	},
				template: '<button @click="accessRoot">访问根实例</button>'
			})

			new Vue({
			  el: '#app',
			  data: {
			  	price: 98
			  },
			  computed: {
			  	totalPrice(){
			  		return this.price * 10;
			  	}
			  },
			  methods: {
			  	hello(){
			  		return "Hello, Vue.js无难事";
			  	}
			  }
			})
		</script>
	</body>
</html>

不管组件是根实例的子组件,还是更深层级的后代组件,$root属性总是代表了根实例。

访问父组件实例

和$root类似,$parent属性用于在一个子组件访问父组件的实例。这可以替代父组件将数据以 prop 的方式传入子组件的方式。

在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候。当我们稍后回看那个组件的时候,很难找出那个变更是从哪里发起的。
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title></title>
		<script src="vue.js"></script>
	</head>
	<body>
		<div id="app">
			<parent></parent>
		</div>
	
		<script>
			Vue.component('parent', {
				data(){
					return {
						price: 98
					}
			  },
			  computed: {
			  	totalPrice(){
			  		return this.price * 10;
			  	}
			  },
			  methods: {
			  	hello(){
			  		return "Hello, Vue.js无难事";
			  	}
			  },
				template: '<child></child>',
			});
			Vue.component('child', {
			 	methods: {
					accessParent(){
						console.log("单价:" + this.$parent.price);
						console.log("总价:" + this.$parent.totalPrice);
						console.log(this.$parent.hello());
					}
				},
				template: '<button @click="accessParent">访问父组件实例</button>'
			})

			new Vue({
			  el: '#app',
			  
			})
		</script>
	</body>
</html>

访问子组件实例或子元素

在Vue.js中,父组件要访问子组件实例或子元素,可以给子组件或子元素添加一个特殊的属性ref, 为子组件或子元素分配一个引用ID ,然后父组件就可以通过$ref属性来访问子组件实例或子元素。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title></title>
		<script src="vue.js"></script>
	</head>
	<body>
		<div id="app">
			<parent></parent>
		</div>
	
		<script>
			Vue.component('parent', {
			  mounted(){
			  	// 访问子元素<input>,让其具有焦点
			  	this.$refs.inputElement.focus();
			  	// 访问子组件<child>的message数据属性
			  	console.log(this.$refs.childComponent.message)
			  },
				template: `
					<div>
						<input ref="inputElement"><br> <!--子元素-->
						<child ref="childComponent"></child> <!-- 子组件-->
					</div>`
			});
			Vue.component('child', {
			 	data(){
			 		return {
			 			message: 'Vue.js无难事'
			 		}
			 	},
				template: '<p>{{message}}</p>'
			})

			new Vue({
			  el: '#app',
			  
			})
		</script>
	</body>
</html>

要注意,$ref属性只在组件渲染完成之后生效,并且他们不是响应式的。要避免在模板和计算属性中访问$refs 。

依赖注入

$root属性用于访问根实例,$parent属性用于访问父组件实例,但如果组件嵌套的层级不确定,某个组件的数据或方法需要被后代组件所访问,又该如何实现呢 ?

这时需要用到两个新的实例选项:provide和inject .provide选项允许我们指定要提供给后代组件的数据或方法,在后代组件中使用inject选项来接收要添加到该实例中的特定属性。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title></title>
		<script src="vue.js"></script>
	</head>
	<body>
		<div id="app">
			<parent></parent>
		</div>
		
		<script>
			Vue.component('parent', {
			  methods: {
			  	sayHello(name){
			  		console.log("Hello, " + name);
			  	}
			  },
			  provide(){
			  	return {
			  		// 数据属性message和sayHello方法可供后代组件访问
			  		message: 'Vue.js无难事',
			  		hello : this.sayHello
			  	}
			  },
				template: '<child/>'
			});
			Vue.component('child', {
				// 接收message数据属性和hello方法
			 	inject: ['message', 'hello'],
			 	mounted(){
			 		// 当自身的方法来访问
			 		this.hello('zhangsan');
			 	},
			 	// 当自身的数据属性来访问
				template: '<p>{{message}}</p>'
			})

			new Vue({
			  el: '#app',
			  
			})
		</script>
	</body>
</html>

手动监听

现在,你已经知道了 $emit 的用法,它可以被 v-on 侦听,但是 Vue 实例同时在其事件接口中提供了其它的方法。我们可以:

  • 通过 $on(eventName, eventHandler) 侦听一个事件
  • 通过 $once(eventName, eventHandler) 一次性侦听一个事件
  • 通过 $off(eventName, eventHandler) 停止侦听一个事件

你通常不会用到这些,但是当你需要在一个组件实例上手动侦听事件时,它们是派得上用场的。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title></title>
		<script src="vue.js"></script>
	</head>
	<body>
		<div id="app">
			<child></child>
		</div>
	
		<script>
			Vue.component('child', {
				created(){
					// 监听当前实例的greet事件
			 		this.$on("greet", function(){
			 			this.$parent.sayHello();
			 		})
			 	},
			 	beforeDestroy(){
			 		// 删除greet事件的所有监听器
			 		this.$off("greet");
			 	},
			 	methods: {
					handleClick(){
						// 触发自定义事件greet
						this.$emit('greet');
					}
				},
				template: '<button @click="handleClick">手动监听事件</button>'
			})

			var vm = new Vue({
			  el: '#app',
			  methods: {
			  	sayHello(){
			  		alert("Hello, Vue.js");
			  	}
			  }
			})
		</script>
	</body>
</html>

在create钩子中使用$on()方法监听当前组件实例的自定义事件greet,在beforeDestory钩子中使用$off()方法删除greet事件的所有监听器。

递归组件

组件是可以在它们自己的模板中调用自身的。不过它们只能通过 name 选项来做这件事:

 name: 'unique-name-of-my-component'

当你使用 Vue.component 全局注册一个组件时,这个全局的 ID 会自动设置为该组件的 name 选项。

Vue.component('unique-name-of-my-component', {
  // ...
})

稍有不慎,递归组件就可能导致无限循环:

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

类似上述的组件将会导致“max stack size exceeded”错误,所以请确保递归调用是条件性的 (例如使用一个最终会得到 false 的 v-if)。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<script src="vue.js"></script>
	</head>
	<body>
		<div id="app">
			<category-component :list="categories"></category-component>
		</div>
	
		<script>
			
			new Vue({
			  el: '#app',
			  data: {
			  	categories: [
			  		{
			  			name: '程序设计', 
			  			children: [
			  				{
			  					name: 'Java', 
			  					children: [
			  						{name: 'Java SE'}, 
			  						{name: 'Java EE'}
			  					]
			  				}, 
			  				{
			  					name: 'C++'
			  				}
			  			]
			  		},
	  				{
	  					name: '前端框架', 
	  					children: [
	  						{name: 'Vue.js'}, 
	  						{name: 'React'}
	  					]
	  				}
			  	]
			  },
			  components: {
			  	CategoryComponent : {
			  		name: 'catComp',
			  		props: {
			  			list: {
			  				type: Array
			  			}
			  		},
				  	data: function(){
							return {
								count: 0
							}
						},
			  		template: `
			  			<ul>
			  				<!-- 如果list为空,表示没有子分类了,结束递归 -->
			  				<template v-if="list">
									<li v-for="cat in list">
										{{cat.name}}
										<catComp :list="cat.children"/>
									</li>
								</template>
			  			</ul>`
			  	}
			  }
			})
		</script>
	</body>
</html>

模板定义的其他方式

内联模板

当 inline-template 这个特殊的 attribute 出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。这使得模板的撰写工作更加灵活。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<script src="vue.js"></script>
	</head>
	<body>
		<div id="app">
			<button-counter inline-template>
				<button v-on:click="count++">You clicked me {{ count }} times.</button>
			</button-counter>
		</div>
	
		<script>
			Vue.component('ButtonCounter', {
				data: function(){
					return {
						count: 0
					}
				}
				// 没有了template选项
  		});
			new Vue({
			  el: '#app'
			})
		</script>
	</body>
</html>

内联模板需要定义在 Vue 所属的 DOM 元素内。

不过,inline-template 会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 <template> 元素来定义模板。

X-Template

另一个定义模板的方式是在一个 <script> 元素中,并为其带上 text/x-template 的类型,然后通过一个 id 将模板引用过去。例如:

 
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<script src="vue.js"></script>
	</head>
	<body>
		
		<div id="app">
			<button-counter></button-counter>
		</div>
		
		<script type="text/x-template" id="btn-counter">
			<button v-on:click="count++">You clicked me {{ count }} times.</button>
		</script>
		<script>
			Vue.component('ButtonCounter', {
				data: function(){
					return {
						count: 0
					}
				},
				template: '#btn-counter'
  		});
			new Vue({
			  el: '#app'
			})
		</script>
	</body>
</html>

这些可以用于模板特别大的 demo 或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开。

异步更新队列

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<script src="vue.js"></script>
	</head>
	<body>
		<div id="app">
			<my-component></my-component>
		</div>
	
		<script>
			Vue.component('my-component', {
				data: function(){
					return {
						message: 'Vue.js无难事'
					}
				},
				methods: {
					change(){
						this.message = 'VC++深入详解';
						console.log(this.$refs.msg.textContent);

					}
				},
	      template: `
	      	<div>
	      		<p ref="msg">{{ message }}</p>
	      		<button v-on:click="change">修改内容</button>
	      	</div>`
  		});
  		
  
			new Vue({
			  el: '#app'
			})
		</script>
	</body>
</html>

以上代码,按理说,<p>元素的内容就是message属性的值,修改了message属性的值,在change()方法中理应输出修改后的值,但实际上输出的是Vue.js无难事。

这是因为Vue在数据变化需要更新DOM时并不是同步执行,而是异步执行的。每当侦听到数据更改时,Vue将开启一个队列。

当在change方法中修改message属性值的时候,该组件不会立即重新渲染。当队列刷新时,组件会在下一个tick中更新。

如果想在数据更新后,立即访问更新后的DOM ,可以用Vue.nextTick方法。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<script src="vue.js"></script>
	</head>
	<body>
		<div id="app">
			<my-component></my-component>
		</div>
	
		<script>
			Vue.component('my-component', {
				data: function(){
					return {
						message: 'Vue.js无难事'
					}
				},
				methods: {
					change(){
						this.message = 'VC++深入详解';
						//Vue.nextTick(() => console.log(this.$refs.msg.textContent))
						this.$nextTick(function(){
							console.log(this.$refs.msg.textContent);
						})
					}
				},
	      template: `
	      	<div>
	      		<p ref="msg">{{ message }}</p>
	      		<button v-on:click="change">修改内容</button>
	      	</div>`
  		});
  		
  
			new Vue({
			  el: '#app'
			})
		</script>
	</body>
</html>