第三章 深入理解Vue组件
组件使用中的细节点
解析 DOM 模板时
<div id="app">
<table>
<tbody>
<row></row>
</tbody>
</table>
</div>
<script>
Vue.component('row',{
template:"<tr><td>this is a row</td></tr>"
})
new Vue({
el:"#app"
})
</script>
我们会发现,执行上面的代码,虽然效果有了,但是这个自定义组件 <row>
会被作为无效的内容提升到外部,并导致最终渲染结果出错。
要解决上面的问题,Vue提供了is
特性。把前面的代码改成如下就可以解决了。
<div id="app">
<table>
<tbody>
<!-- 正确的写法 -->
<tr is="row"></tr>
</tbody>
</table>
</div>
<script>
Vue.component('row',{
template:"<tr><td>this is a row</td></tr>"
})
new Vue({
el:"#app"
})
</script>
除了上面提到的HTML元素,还有其它的元素也得必须记住,在使用到这样元素的时候,也需要改成上面的写法。这些元素有<ul>
、<ol>
、<table>
和 <select>
,它们对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li>
、<tr>
和 <option>
,只能出现在其它某些特定的元素内部。
组件中的data必须是一个函数
因为组件是可复用的 Vue 实例,所以它们与 new Vue
接收相同的选项,例如 data
、computed
、watch
、methods
以及生命周期钩子等。仅有的例外是像 el
这样根实例特有的选项。
下面的这段代码与前面的理论上是一样,但是其实这是错误的写法。因为Vue规定,在组件中data
必须是一个函数。
<div id="app">
<table>
<tbody>
<!-- 正确的写法 -->
<tr is="row"></tr>
</tbody>
</table>
</div>
<script>
Vue.component('row',{
//错误示范
data:{
content:'this is a row'
},
template:"<tr><td>{{content}}</td></tr>"
})
new Vue({
el:"#app"
})
</script>
所以要解决上面的问题,只需将data
改写成函数就可以了。
<div id="app">
<table>
<tbody>
<!-- 正确的写法 可以多次调用 且互不影响-->
<tr is="row"></tr>
<tr is="row"></tr>
<tr is="row"></tr>
</tbody>
</table>
</div>
<script>
Vue.component('row',{
//正确的写法
data:function(){
return {
content:"this is a row"
}
},
template:"<tr><td>{{content}}</td></tr>"
})
new Vue({
el:"#app"
})
</script>
Vue 之所以这么设计,是因为子组件它不像根组件(new Vue
),它只调用一次。而我们希望的是,被调用的地方它们都应该有自己的数据。通过一个函数来返回一个对象的目的就是让子组件都拥有独立的数据存储。这样就不会出现子组件相互影响的情况了。
Vue中关于ref引用
我们都知道Vue中不建议我们操作DOM,但是在处理一些极其复杂的动画效果,光使用Vue中的数据绑定是处理不好这样的情况的。所以在某些情况下我就就不得不操作DOM。那么要如何操作DOM呢?
我们需要使用ref
这中引用的方式来进行DOM操作。
需求:点击页面上的内容,并打印输出。
思路:要实现起来只需要获得页面上的元素内容,就可以实现了。
如下的代码使用ref
就可以获得页面上的元素内容。
<div id="app">
<div ref='hello' @click="clickme">快点我看看效果</div>
</div>
<script>
new Vue({
el:"#app",
methods:{
clickme:function(){
console.log(this.$refs.hello.innerHTML);//快点我看看效果
}
}
})
</script>
但是有一个问题,如果把DIV
换成我们自定义的组件又是获取到什么呢?其实获取的是组件的引用。
ref在组件里应用
需求:写一个计数器,点击页面上的内容1,且可以自加,同理也存在一个内容2。把内容1和内容2相加并显示出来。
思路:定义个组件,重复调用两次该组件。通过ref
来获取组件上的内容。
<div id="app">
<row ref="one" @change="add"></row>
<row ref="two" @change="add"></row>
<div>{{total}}</div>
</div>
<script>
Vue.component('row',{
data:function(){
return {
number: 0
}
},
template:"<div @click='change'>{{number}}</div>",
methods:{
change:function(){
this.number++;
this.$emit('change');
}
}
})
new Vue({
el:"#app",
data:{
total: 0
},
methods:{
add:function(){
this.total = this.$refs.one.number + this.$refs.two.number;
}
}
})
</script>
父子组件间的传值
父组件如何向子组件传递数据呢?父组件通过属性的形式向子组件传递数据。
<div id="app">
<!-- 提示:count的值是字符串类型还是数值类型取决于count前面有没有‘:’ -->
<!-- 含义:父组件[<div id="app">]给子组件[<counter>]传递了一个count属性 -->
<counter :count="0"></counter>
<counter :count="0"></counter>
</div>
<script>
//定义个局部组件
var counter = {
//子组件使用props接收传递过来的属性
props: ['count'],
template: "<div @click='change'>{{count}}</div>",
methods: {
change:function(){
this.count++;
}
}
}
var vm = new Vue({
el: "app",
//定义的局部组件需要在Vue实例中进行注册才会有效
components:{
counter:counter
}
})
</script>
通过上面代码中,给template
中的div
绑定了一个点击事件并对传递过来的数值进行加一,发现也可以改变count的值。但是系统会提示警告。为什么会提示警告呢?这是因为 Vue 中有一个单向数据流
的概念。
**单向数据流:**父组件可以向子组件传递任何值,但是子组件不能直接修改父组件传递过来的值。如果要修改,请使用传递事件的方法。
那么要解决以上的单向数据流的问题,其实也很简单。只需要在子组件中多写一个data
属性就可以了。
<script>
//定义个局部组件
var counter = {
//子组件使用props接收传递过来的属性
props: ['count'],
//前面已经说过,组件中的data属性必须是一个函数
data:function(){
return {
number:this.count
}
},
//这行也要改成{{number}}
template: "<div @click='change'>{{number}}</div>",
methods: {
change:function(){
this.number++;
}
}
}
</script>
上面我们已经知道了父组件是如何给子组件传值的,那么子组件又是如何向父组件传值的呢?子组件通过事件还父组件传值。
<div id="app">
<!-- 2.在这里对子组件的事件进行监听 -->
<counter :count="0" @childchange="add"></counter>
<counter :count="0" @childchange="add"></counter>
<!-- 显示总和 -->
<div>{{total}}</div>
</div>
<script>
var counter = {
props: ['count'],
data:function(){
return {
number:this.count
}
},
template: "<div @click='change'>{{number}}</div>",
methods: {
change:function(){
this.number++;
//1.当子组件被点击的时候,可以通过$emit()向外触发一个事件,且可以传递多个参数
//坑:'childchange' 这个命名不能用驼峰写法
this.$emit('childchange',1);//第二个参数表示每次增加了1
}
}
}
var vm = new Vue({
el: "#app",
data:{
total: 0
},
components:{
counter:counter
},
//3.最后在子组件接受到的事件写相应的功能,参数val接收$emit()第二个参数传递过来的值
methods:{
add:function(val){
this.total += val;
}
}
})
</script>
非父子组件间传值
什么是非父子组件?通过下面这张图我们知道,1与2,2与3 都是属性父子组件的关系。那么1与3、2与2、3与3就是属性非父子组件的关系了。前面我们已经知道了父子组件间是如何传值的了,那么非父子组件又是怎么样传值的呢?
非父子组件间传值,有两种方法。第一种借助Vue官方提供的Vuex框架。但是现在我们暂时先说一下第二种方法:通过(Bus\总线\发布订阅模式\观察者模式)。接下来我们来通过代码来说明。
需求:在页面上有两个文字内容,点击文字1使 文字2=文字1 或 点击文字2使 文字1=文字2
<div id="app">
<child content='mike'></child>
<child content='json'></child>
</div>
<script>
//1.给Vue添加一个bus属性,作用在于只要调用了Vue实例还是组件,里面都有会bus这个属性。它们指向的都是同一个Vue
Vue.prototype.bus = new Vue();
Vue.component('child',{
//3.因为子组件不允许直接修改父组件内容 需要接受并拷贝一份
data:function(){
return {
selfcontent:this.content
}
},
props:['content'],
//2.要点击子组件就需要给它绑定一个点击事件
template:"<p @click='clickme'>{{selfcontent}}</p>",
methods:{
clickme:function(){
//4.因为第一步我们添加了一个bus,所以bus相当于是个vue,所以可以通过$emit()方法向外触发一个事件并把当前值传递出去。
this.bus.$emit('change',this.selfcontent);
}
},
//5.借助生命周期函数,当vue被挂载的时候我们就监听第四步的事件。
mounted:function(){
var _this = this;
//注意:这里的function会使this指向改变
this.bus.$on('change',function(msg){
_this.selfcontent = msg;
})
}
})
var vm = new Vue({
el:'#app'
})
</script>
组件参数校验与非props特性
组件参数校验
<div id="app">
<child content="你好" name="mike json" :age="20"></child>
</div>
<script>
Vue.component("child",{
props:{
//只接受XX类型
age: Number,
//可接受多种XX类型 String、Number、Boolean、Array、Object、Date、Function、Symbol
content: [Number,String,Object],
//高级数据校验写法
name:{
type: String,
required: true, //为真表示必须传值 即组件中不能没有content
default: "如果required为假,显示这句",
//自定义数据校验 value为表示接受到的值
validator: function(value){
return (value.length > 5) //传的值长度大于5个字符 否则报错
}
}
},
template:"<p>{{content}}{{name}}{{age}}</p>"
});
new Vue({
el:'#app'
})
</script>
props特性与非props特性
props特性:父组件调用子组件并且向子组件通过属性传递了值,恰好在组件声明里面也接受了父组件传过来的属性。
props特点:1. 父组件向子组件传递的属性不会显示在DOM结构上。2. 传递过来属性,可以通过插值表达式或this.属性
的形式获取属性的值。
非props特性:父组件调用子组件并且向子组件通过属性传递了值,但是在组件声明里面并没有接收。即没有声明props的属性。
非props特点:1.父组件向子组件传递的属性会显示在DOM结构上。2.如果template
引用了父组件传递过来的属性,系统会报错。
给组件绑定原生事件
方法一:太啰嗦写法
这种写法在前面也写了几遍,我们会发现,需要在组件里写methods
也要在 Vue 实例中写methods
。
<div id="app">
<child @childclik="handleClick"></child>
</div>
<script>
Vue.component("child",{
//给组件绑定原生事件的第一种写法
template:"<p @click='originClick'>clickme</p>",
methods:{
originClick:function(){
this.$emit('childclik');
}
}
});
new Vue({
el:'#app',
methods:{
handleClick:function(){
console.log('childClik');
}
}
})
</script>
方法二:使用.native
修饰符
该方法可以让你少些许多代码,却一目了然。
<div id="app">
<child @click.native="handleClick"></child>
</div>
<script>
Vue.component("child",{
//给组件绑定原生事件的第一种写法
template:"<p>clickme</p>",
});
new Vue({
el:'#app',
methods:{
handleClick:function(){
console.log('childClik');
}
}
})
</script>
在Vue中使用插槽(slot)
和 HTML 元素一样,我们经常需要向一个组件传递内容,像这样:
<div id="app">
<child>
<!-- 给标签定义一个slot唯一标识,否则会渲染多次-->
<div class="header" slot="header">header</div>
<div class="footer" slot="footer">footer</div>
</child>
</div>
<script>
//如果我们想让
Vue.component('child',{
//这里使用name来获取相应的内容 这种叫做具名插槽
tempalte:`<div>
<slot name='header'></slot>
<div class="content">content</div>
<slot name='footer'></slot>
</div>`
})
new Vue({
el:'#app'
})
</script>
在向具名插槽提供内容的时候,我们可以在一个 <template>
元素上使用 v-slot
指令,并以 v-slot
的参数的形式提供其名称:
<child>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
</child>
注意 v-slot
只能添加在 <template>
上,这一点和已经废弃的 slot
特性不同
作用域插槽
使用作用域插槽,子组件可以向父组件传数据,父组件要接收传递过来的数据必须使用<template>
并且使用v-slot
来接收传递过来的数据。
<div id="app">
<child>
<template v-slot:default='slotProps'>
<li>{{slotProps.item}}</li>
</template>
</child>
</div>
<script>
Vue.component('child',{
data:function(){
return {
list:[1,2,3,4,5,6]
}
},
template:`<div>
<ul>
<slot v-for="item of list" :item=item></slot>
</ul>
</div>`
})
var vm=new Vue({
el:'#app'
})
</script>
动态组件
<component>
是系统内置的标签,它指的就是一个动态组件。使用这个组件,可以轻松的实现来回切换的效果。
is
是一个特殊特性,用于动态组件且基于 DOM 内模板的限制来工作。
<div id="app">
<!-- 当 `type` 改变时,组件也跟着改变 -->
<component :is='type'></component>
<button @click="clickme">切换</button>
</div>
<script>
Vue.component('child-one',{
template:'<div>child-one</div>'
})
Vue.component('child-two',{
template:'<div>child-two</div>'
})
var vm=new Vue({
el:'#app',
data:{
type:'child-one'
},
methods:{
clickme:function(){
this.type = this.type === 'child-one' ? 'child-two' : 'child-one'
}
}
})
</script>