(day8-2)
在前面我们其实已经接触到了一些关于组件的知识。
关于它的定义,创建,以及简单的使用。
组件是可复用的Vue实例,且带有一个名字(Vue.component()中的第一个参数)。
在实际的开发过程中,可能会遇见很多重复的功能,我们可以封装这么一些Vue组件,来重复使用。
因为这个组件是可复用的Vue实例,所以它与 new Vue 接收相同的选项,例如 data 、computed 、watch 、methods 以及生命周期钩子等。仅有的例外是像 el 这样根实例特有的选项。
组件的复用、组件的组织
通过Props向子组件传递数据、单个根元素
监听子组件事件
通过插槽分发内容
动态组件
解析DOM模板时的注意事项
一、组件的复用、组件的组织
1、组件的复用
可以将组将进行任意次数的复用
<div>
<my-component></my-component>
<my-component></my-component>
<my-component></my-component>
<my-component></my-component>
</div>
每个组件都会各自维护它的count。因为你每用一次组件,就会有一个它的新实例被创建。
# data 必须是一个函数
我们知道,定义组件时Vue.component()第二个参数,接收的选项和 new Vue 是一样的。但是,组件中 data 选项并不是像这样直接提供一个对象。
取而代之的是,一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立拷贝。data:function(){ return { count:0 } }
如果Vue没有这条规则,也就是说 data 不是一个函数,而是一个对象,那么每一个组件实例,他们共享着这个 data 对象的数据,所以这些不同组件中的数据变化将会相互的影响。
2、组件的组织
前面第一天已经了解过了,一般,一个应用会以一颗嵌套的组件树的形式来组织:
为了能在模板中使用,这些组件必须先注册以便Vue能够识别。这里有两种组件的注册类型:全局注册和局部注册。
到现在为止,我们的组件都是通过 Vue.component 全局注册的。
全局注册的组件可以在被注册之后,可以被任何新创建的Vue根实例使用,包括上图组件树中的所有子组件的模板中。
现在只需要知道这么多,之后在后面的 深入了解组件——组件注册 会有更详细的介绍。
二、通过Prop向子组件传递数据、单个根元素
1、通过Prop向组件传递数据
在前面我们已经接触到的创建组件中,我们知道我们的自定组件是一个Vue实例,如果不将Vue数据传递给我们创建的自定义组件中,我们的自定义组件是没有数据的。
Prop是你可以在组件上注册的一些自定义attribute。当一个值传递给prop attribute时,它就变成了那个组件实例的一个property。
一个组件默认可以拥有任意数量的prop,任何值都可以传递给任何prop。
就比如说前面 v-for 循环创造模板组件的,需要通过设定一个props 值,来和定义在控制着元素的Vue实例中的数组项 绑定,从而读取到该数组项的数据。 (具体的实例在前面其实就已经有了,这里就不赘述了)
# 当使用自定义组件的属性 绑定 的内容很复杂,写起来会非常麻烦:
<todo-item v-for="post in posts"
v-bind:key="posts.id"
v-bind:title="posts.title"
v-bind:content="posts.content"
v-bind:publishAt="posts.publishAt"
v-bind:comment="posts.comment"
>
</todo-item>
这样写就是这么一个意思:
这用一个一个的绑定有点麻烦,其实可以直接在props中定义一个 “post” 属性,然后绑定与 post 绑定就好了:
<todo-item v-for="post in posts"
v-bind:key="post.id"
v-bind:post="post">
</todo-item>
这样写就很简单了,大概是这么个意思:
到目前为止只要知道这多就行了,后面还会继续讨论这个 组件属性。
2、单个根元素
在自定义组件中,用 template 属性来定义我们自定义组件的模板,如果这个模板比较复杂,需要多个HTML元素,那么这个时候,一定要记住给模板中的所有元素外包一个根元素。否则会报错。
Vue.component("todo-item",{
template:`
<div class="blog-post">
<h3>{{ post.title}}</h3>
<div v-html="post.content"></div>
</div>
`
});
这里的 <div class="blog-post"></div> 就是我们的根元素。
注意:这里的 template 中定义的模板使用了JavaScript的模板字符串来让多行的模板更易读。
三、、监听子组件事件
很简单,前面我们学过 v-on: 指令,在前面的所有例子中, v-on: 后面接的都是我们在“DOM3级事件”所定义的事件类型之类的事件(比如click、keyup等)。那如果我们需要触发自定义事件怎么办?(也就是我们需要触发事件的事件名时我们自己定义的,比如enlarge-text)
自定义事件的详细知识在“JavaScript高级程序设计”里面有讲解,这里就不去赘述。
假设:
//1-HTML中的元素
<div id="blog-post-demo">
<div :style="{fontSize:postFontSize +'em'}">
<blog-post v-for="post in posts"
v-bind:key="post.id"
v-bind:post="post">
<blog-post>
</div>
</div>
//2-用一个Vue实例绑定最外层的div,从而完全控制它的行为属性
var blog-post-demo = new Vue({
el:"#blog-post-demo",
data:{
posts:[/*...*/],
postFontSize:1
}
});
//3-定义模板中的点击事件
Vue.component('blog-post',{
props:['post'],
tempalte:`<li>
<button v-on:click="enlarge-text">Enlarge Text</button>
</li>`
});
假设在 Vue自定义组件 中,有一个按键 <button> 元素,这个按键按下就会触发一个click事件,当这个click事件触发,就会调用一个方法——假设为 enlarge-text ,像上面那样。在这里我们的想法是——enlargeText方法会将在HTML中的元素字体增大,上面第三步中的组件定义可以添加上enlarge-text函数定义:
//3-定义模板中的点击事件
Vue.component('blog-post',{
props:['post'],
tempalte:`<li>
<button v-on:click="enlarge-text">Enlarge Text</button>
</li>`,
methods:{
enlarge-text:function(){
postFontSize +=0.1;
}
}
});
这样一来,我们定义了一个 <div> 元素,<div>元素里面有这么些个自定义组件,自定义组件里面都有一个按键,按键按下,会触发click事件,调用enlarge-text函数,这个函数使整个 组件 的字体变大。一切的顺理成章,不是吗?
但是我们忽略了一点,在第三步,自定义组件中的enlarge-text方法,为了使整个 组件中的字体变大,会调用 postFontSize 属性,但是在这个Vue.component()定义的Vue组件中,有这个属性的值吗?没有,因为每一个组件都是Vue实例,在这个 blog-post 实例中,并没有定义 postFontSize 的值,在这个组件中,postFontSize 的值为undefined,所以直接在组件中这修改postFontSize 的值 "postFontSize +=0.1;" 是错误的,达不到效果。
怎么办?
监听子组件事件。
这么说有些云里雾里的。来看我们上面的矛盾是什么,是在定义组件时,我们想要通过组件中的元素的点击事件来改变父级元素中的属性。简单的说,这里的组件是“爸爸”,组件中的按钮是“儿子”,组件外层的<div> 是“爷爷”。按儿子一下,就要爸爸中所有的字体都变大,这个变大效果需要儿子去改变一个 postFontSize 属性,但是爸爸里面没有这个 postFontSize 属性,为什么呢?因为在JavaScript中时,爸爸和爷爷是两个不同的Vue实例,这个时候爸爸还没有变成这个爷爷的儿子,他们俩没关系(在上面的HTML 定义中,爸爸才变成了爷爷的儿子。在这个HTML中,爷爷包括爸爸,爸爸包括儿子。),爸爸里面没有定义这个属性,也访问不到爷爷中定义的这个属性。
这个时候,儿子就会给爸爸说:“爸爸,我想让爸爸你中的所有字体都变大,但是我改不了postFontSize的值。” 爸爸听到了,知道儿子想要做一件事——enlarge-text,ok,爸爸去做这件事,因为在HTML中,爸爸找到了爷爷,爷爷给了爸爸一个postFontSize 的属性值。
这里就会发生一些改变,儿子发生了click事件以后,不再是调用方法直接去处理事件,而是创建一个事件——"enlarge-text",然后爸爸在HTML 通过 v-on:enlarge-text=“..”,来监听儿子的这个事件并作出处理。说简单就是,想要变大字体的了,儿子你不要做,直接告诉爸爸你想要干嘛,让爸爸来做。
这就是监听子组件事件。
上面的代码就要这样改一下:
//1-HTML中的元素
<div id="blog-post-demo">
<div :style="{fontSize:postFontSize +'em'}">
<blog-post v-for="post in posts"
v-bind:key="post.id"
v-bind:post="post"
v-on:enlarge-text="postFontSize += 0.1">
<blog-post>
</div>
</div>
//2-用一个Vue实例绑定最外层的div,从而完全控制它的行为属性
var blog-post-demo = new Vue({
el:"#blog-post-demo",
data:{
posts:[/*...*/],
postFontSize:1
}
});
//3-定义模板中的点击事件
Vue.component('blog-post',{
props:['post'],
tempalte:`<li>
<button v-on:click="$emit('enlarge-text')">Enlarge Text</button>
</li>`
});
这里的 $emit() 方法就是将enlarge-text 定义为一个自定义事件。使父级组件能够通过 v-on: 监听。
# vm.$emit(eventName,[...args])
- 参数:
- {string} eventName
- {...args}
触发当前实例上的事件。附加参数都会传给监听器回调。- 示例:
- 只配合一个事件名使用 $emit ,这里就不细讲这种情况了,上面的例子就是这种用法。
- 配合额外的参数使用 $emit :
Vue.component('magic-eight-ball', { data: function () { return { possibleAdvice: ['Yes', 'No', 'Maybe'] } }, methods: { giveAdvice: function () { var randomAdviceIndex = Math.floor(Math.random() * this.possibleAdvice.length) this.$emit('give-advice', this.possibleAdvice[randomAdviceIndex]) } }, template: ` <button v-on:click="giveAdvice"> Click me for advice </button> `
<div id="emit-example-argument"> <magic-eight-ball v-on:give-advice="showAdvice"></magic-eight-ball> </div>
new Vue({ el: '#emit-example-argument', methods: { showAdvice: function (advice) { alert(advice) } } })
这里在使用 $emit 的时候,传入了第二个参数,这个参数会在监听这个事件的监听器中回调。(即会在showAdivice中回调 give-advice事件中的this.possiveAdvice[randomAdviceIndex]值)
这里的$emit()方法的详细实现过程应该是JavaScript中的自定事件,需要以后去搞明白这里Vue的$emit()具体实现过程。
1、使用事件抛出一个值
也就是给$emit()方法传入第二个参数。
<button v-on:click="$emit('enlarge-text',0.1)">
Enlarge Text
</button>
然后在父级组件中通过 $event 访问到被抛出的值:
<blog-post v-on:enlarge-text="postFontSize += $event"></blog-post>
或者再父级组件中这个事件处理函数是一个方法,这个值将作为第一个参数传入这个方法(就像上面块引用中情况):
<blog-post v-on:enlarge-text="onEnlargeText"></blog-post>
methods:{
onEnlargeText:function(enlargetAmount){
this.postFontSize += enlargeAmout;
}
}
2、在组件上使用 v-model
利用自定义事件 创建 支持v-model 的自定义输入组件。
我们知道表单输入组件是支持 v-model 的:
<input v-model="searchText">
这个等价于:
<input v-bind:value="searchText" v-on:input="searchText = $event.target.value">
当在组件上使用 v-model 的时候,在 v-model 之下的操作并不是上面这种等价于,而是这样的底层操作:
<custom-input v-model="searchText">
此时的操作其实等价于:
<custom-input v-bind:value="searchText" v-on:input="searchText=$event"><custom-input>
这就不对了呀,你看我们的v-model 等价的操作第二步是 v-on:input="searchText=$event.target.value",而这里的却是 v-on:input="searchText=$event"。既然在自定义输入组件中使用 v-model 指令的操作和普通表单输入组件中的不一样,那么我们就用一个操作使他们一样。
仔细观察,我们这里差的其实是 赋给 searchText 的值,普通的是 $event.target.value,而自定义组件是 $event。
而在自定义事件中,父级组件就是通过$event来访问 $emit() 第二个参数值。
这就简单了啊,通过自定义事件$emit()的第二个参数,将 $event.target.value 传递给 父级组件的$event。
Vue.component("custom-input",{
props:['value'],
template:`<input v-bind:value="value"
v-on:input="$emit('input',$event.target.value)">`
});
这里就先了解到这,还有一些其他内容将在下一章“深入了解组件”中继续学习。
四、通过插槽分发内容
和HTML 一样,我们经常需要向一个组件传递内容:
<alert-box>
Somthing bad happend
</alert-box>
这样渲染可能不好。而可以通过Vue 元素的 <slot></slot> 来完成这个插入内容的操作:
Vue.component("alert-box",{
template:`<div class="demo-alert-box">
<strong>Error!</strong>
<slot></slot>
</div>`
});
在 slot 中插入想要插入的内容。
到目前为止就只需要了解这些。下一章还要深入了解它。
五、动态组件
就是通过Vue 的 <component> 元素加一个特殊的 is attribute来实现的:
<!--组件会在‘currentTabComponent’改变时改变-->
<component v-bind:is="currentTabComponent"></component>
在上述实例中, currentTabComponent 可以包括:
- 已经注册组件的名字,或
- 一个组件的选项对象
请留意,这个 attribute 可以用于常规 HTML 元素,但这些元素将被视为组件,这意味着所有的 attribute 都会作为 DOM attribute 被绑定。对于像 value
这样的 property,若想让其如预期般工作,你需要使用 .prop
修饰器。
这里先了解到这。
六、解析DOM模板时的注意事项
有些HTML 元素,如 <ul>、<ol>、<table>、<select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li>
、<tr>
和 <option>
,只能出现在其它某些特定的元素内部。
这会导致一些问题的出现:
<table>
<blog-post-row></blog-post-row>
<table>
这个自定义组件 <blog-post-row>
会被作为无效的内容提升到外部,并导致最终渲染结果出错。幸好这个特殊的 is
attribute 给了我们一个变通的办法:
<table>
<tr is="blog-post-row"></tr>
</table>
上面这些就是 组件的基础知识:
组件注册、组件复用、组件结构、通过Prop向子组件传递数据、单个根元素、监听子组件事件、同通过插槽分发内容、动态组件、解析DOM模板时的注意事项。
在下一章深入的了解这些内容。