1.组件(Component)是Vue.js最核心的功能。
2.组件的注册有全局注册和局部注册两种方式。全局注册后,任何Vue示例都可以使用。
全局注册示例:
Vue.component('my-component',{
//选项
})
my-component 就是注册的组件自定义标签名称,推荐使用小写加减号分割的形式命名。
要在富实例中使用这个组件,必须要在示例创建前注册,之后就可以用<my-component></my-component>的形式来使用组件了,示例:
<div id="app">
<my-component></my-component>
</div>
<script>
Vue.component('my-component',{
template:'<div>这里是组件的内容</div>'
})
var app = new Vue({
el:'#app'
})
</script>
template的DOM结构必须被一个元素饱含,如果直接写内容,不带<div></div> 是无法渲染的。
在Vue实例中,使用components选项可以局部注册组件,注册后的组件只有在该实例作用域下有效。组件中可以使用components选项来注册组件,使组件可以嵌套。示例:
<div id="app">
<my-component></my-component>
</div>
<script>
var child = {
template:'<div>这里是组件的内容</div>'
}
var app = new Vue({
el:'#app',
components:{
'my-component':child
}
})
</script>
3.Vue组件的模版在某些情况下会收到HTML的限制,比如<table>内规定只允许是<tr>、<td>、<th>等这些表格元素,所以在<table>内直接使用组件是无效的。在这种情况下,可以使用 特殊的is属性 来挂载组件,示例:
<div id="app">
<table>
<tbody is="my-component"></tbody>
</table>
</div>
<script>
Vue.component('my-component',{
template:'<div>这里是组件的内容</div>'
})
var app = new Vue({
el:'#app'
})
</script>
tbody在渲染时,会被替换为组件的内容。常见的限制元素还有<ul>、<ol>、<select>。
如果使用的是字符串模版,是不受限制的,比如.vue单文件用法等。
4.除了template选项外,组件中还可以像Vue实例哪样使用其他的选项,比如data、computed、methods等,但是在使用data时,和实例稍有区别,data必须是函数,然后将数据return 出去,例如:
<div id="app">
<my-component></my-component>
</div>
<script>
Vue.component('my-component',{
template:'<div>{{message}}</div>',
data:function(){
return {
message:'组件内容'
}
}
})
var app = new Vue({
el:'#app'
})
</script>
JavaScript对象是引用关系,所以如果return出的对象引用了外部的一个对象,那这个对象就是共享的,任何一方修改都会同步。例如:
<div id="app">
<my-component></my-component>
<my-component></my-component>
<my-component></my-component>
</div>
<script>
var data = {
counter:0
}
Vue.component('my-component',{
template:'<button @click="counter++">{{counter}}</button>',
data:function(){
return data;
}
})
var app = new Vue({
el:'#app'
})
</script>
组件使用了3此,但是点击任意一个button,3个的数字都会加1,那是因为组件的data引用的是外部的对象,这肯定不是我们期望的效果,所以给组件返回一个新的data对象来独立,示例:
<div id="app">
<my-component></my-component>
<my-component></my-component>
<my-component></my-component>
</div>
<script>
Vue.component('my-component',{
template:'<button @click="counter++">{{counter}}</button>',
data:function(){
return {
counter:0
};
}
})
var app = new Vue({
el:'#app'
})
</script>
这样,点击3个按钮就互补影响了,达到了复用的目的。
5.组件不仅仅是要把模版的内容进行复用,更重要的是组件间要进行通信。通常,父组件的模版中包含子组件,父组件要正向地向子组件传递数据或参数,子组件接收后根据参数的不同来渲染不同的内容或执行操作。这个正向传递数据的过程就是通过props来实现的。
6.在组件中,使用选项props来声明须要从父级接收的数据,props的值可以是两种,一种是字符串数组,一种是对象。
7.我们构造一个数组,接受一个来自父级的数据message,并把它在组件模版中渲染,示例代码:
<div id="app">
<my-component message="来自父组件的数据"></my-component>
</div>
<script>
Vue.component('my-component',{
props:['message'],
template:'<div>{{message}}</div>'
})
var app = new Vue({
el:'#app'
})
</script>
props中声明的数据与组件data函数return的数据主要区别就是props的来自父级,而data中的是组件自己的数据,作用域是组件本身,这两种数据都可以在模版template及计算属性computed和方法methods中使用。上例的数据message就是通过props从父级传递过来的,在组件的自定义标签上直接写该props的名称,如果要传递多个数据,在props数组中添加项即可。
8.由于HTML特性不区分大小写,当使用DOM模版时,驼峰命名(camelCase)的props名称要转为短横分割命名(kebab-case),如:
<div id="app">
<my-component warning-text="提示信息"></my-component>
</div>
<script>
Vue.component('my-component',{
props:['warningText'],
template:'<div>{{warningText}}</div>'
})
var app = new Vue({
el:'#app'
})
</script>
9.有时候,传递的数据并不是直接写死的,而是来自父级的动态数据,这时可以使用指令v-bind来动态绑定props的值,当父组件的数据变化时,也会传递给子组件。示例代码:
<div id="app">
<input type="text" v-model="parentMessage">
<my-component :message="parentMessage"></my-component>
</div>
<script>
Vue.component('my-component',{
props:['message'],
template:'<div>{{message}}</div>'
})
var app = new Vue({
el:'#app',
data:{
parentMessage:''
}
})
</script>
这里用v-model绑定了父级的数据parentMessage,当通过输入框任意输入时,子组件接收到的props “message” 也会实时响应,并更新组件模版。
注意,如果要直接传递数字、布尔值、数组、对象,而且不使用v-bind,传递的仅仅是字符串,尝试下面的示例来对比:
<div id="app">
<my-component message="[1,2,3]"></my-component>
<my-component :message="[1,2,3]"></my-component>
</div>
<script>
Vue.component('my-component',{
props:['message'],
template:'<div>{{message.length}}</div>'
})
var app = new Vue({
el:'#app'
})
</script>
同一个组件使用了两次,区别是第二次使用的是v-bind。渲染后的结果是,第一个是7,第二个是才是数组的长度3。
10.Vue2.x通过props传递数据是单向的。也就是父组件数据变化时会传递给子组件,但是反过来步行。
11.业务中经常会遇到两种需要改变prop的情况,一种是父组件传递初始值近来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况可以在组件data内再声明一个数据,引用父组件的prop,示例代码:
<div id="app">
<my-component :init-count="1"></my-component>
</div>
<script>
Vue.component('my-component',{
props:['initCount'],
template:'<div>{{count}}</div>',
data:function(){
return {
count:this.initCount
}
}
})
var app = new Vue({
el:'#app'
})
</script>
组件中声明了数据count,它在组件初始化时会获取来自父组件的initCount,之后就与之无关了,只用维护count,这样就可以避免直接操作initCount。
另一种情况就是prop作为需要被转变的原始值传入。这种情况用计算属性就可以了,例如:
<div id="app">
<my-component :width="100"></my-component>
</div>
<script>
Vue.component('my-component',{
props:['width'],
template:'<div :style="style">组件内容</div>',
computed:{
style:function(){
return {
width: this.width + 'px',
border: '1px solid black'
}
}
}
})
var app = new Vue({
el:'#app'
})
</script>
因为CSS传递宽度要带单位(px),但是每次都写太麻烦,而且数值计算一般是不带单位的,所以统一在组件内使用计算属性就可以了。
注意,在JavaScript中对象和数组是引用类型,指向同一个内存空间,所以props是对象和数组时,在子组件内改变是会影响父组件的。
12.上面介绍的props选项的值都是一个数组,除了数组外,还可以是对象,当prop需要验证时,就需要对象写法。
一般当你的组件需要提供给别人使用时,推荐都进行数据验证,比如某个数据必须是数字类型,如果传入字符串,就会在控制台弹出警告,以下是几个prop的示例:
Vue.component('my-component',{
props:{
//必须是数字类型
propA:Number,
//必须是字符串或数字类型
propB:[String,Number],
//布尔值,如果没有定义,默认值就是true
propC:{
type:Boolean,
default:true
},
//数字,而且是必传
propD:{
type:Number,
required:true
},
//如果是数组或对象,默认值必须是一个函数来返回
propE:{
type:Array,
default:function(){
return [];
}
},
//自定义一个验证函数
propF:{
validator:function(value){
return value > 10;
}
}
}
验证的type类型可以是:
- String
- Number
- Boolean
- Object
- Array
- Function
type也可以是一个自定义构造器,使用instanceof检测。
当prop验证失败时,在开发版本下回在控制台抛出一条警告。
13.Vue组件的通信场景:父子组件通信、兄弟组件通信、跨级组件通信。
14.当子组件需要向父组件传递数据时,就要用到自定义事件。v-on除了监听DOM事件外,还可以用于组件之间的自定义事件。
15.与JavaScript的设计模式——观察者模式中的dispatchEvent和addEventListener这两个方法类似。Vue组件也有一套模式,子组件用$emit()来触发时间,父组件用$on()来监听子组件的事件。
父组件也可以直接在子组件的自定义标签上使用v-on来监听子组件触发的自定义事件,示例代码:
<div id="app">
<p>总数:{{total}}</p>
<my-component
@increase="handleGetTotal"
@reduce="handleGetTotal"></my-component>
</div>
<script>
Vue.component('my-component',{
template:'\
<div>\
<button @click="handleIncrease">+1</button>\
<button @click="handleReduce">-1</button>\
</div>',
data:function(){
return {
counter:0
}
},
methods:{
handleIncrease:function(){
this.counter ++;
this.$emit('increase',this.counter);
},
handleReduce:function(){
this.counter --;
this.$emit('reduce',this.counter);
}
}
})
var app = new Vue({
el:'#app',
data:{
total:0
},
methods:{
handleGetTotal:function(total){
this.total = total;
}
}
})
</script>
上面的例子中,子组件有两个按钮,分别实现加1和减1的效果,在改变组件的data "counter"后,通过$emit()再把它传给父组件,父组件用v-on:increase 和 v-on:reduce (示例使用的是语法糖)。$emit方法的第一个参数是自定义事件的名称,例如示例的increase和reduce后面的参数都是要传递的数据,可以不填或填写多个。
16.除了用 v-on 在组件上监听自定义事件外,也可以监听DOM事件,这时可以用.native 修饰符表示监听的是一个原生事件,监听的是该组件的根元素,示例如下:
<my-component v-on:click.native="handleClick"></my-component>
17.使用 v-model
Vue2.x可以在自定义组件上使用 v-model指令,示例:
<div id="app">
<p>总数:{{total}}</p>
<my-component v-model="total"></my-component>
</div>
<script>
Vue.component('my-component',{
template:'<button @click="handleClick">+1</button>',
data:function(){
return {
counter:0
}
},
methods:{
handleClick:function(){
this.counter ++;
this.$emit('input',this.counter);
}
}
})
var app = new Vue({
el:'#app',
data:{
total:0
}
})
</script>
仍然是点击按钮加1的效果,不过这次组件$emit()的事件名是特殊的input,在使用组件的父级,并没有在<my-component>上使用@input="handler",而是直接用了v-model绑定的一个数据total。这也可以称作是一个语法糖,因为上面的示例可以间接地用自定义事件来实现:
<div id="app">
<p>总数:{{total}}</p>
<my-component @input="handleGetTotal"></my-component>
</div>
<script>
Vue.component('my-component',{
template:'<button @click="handleClick">+1</button>',
data:function(){
return {
counter:0
}
},
methods:{
handleClick:function(){
this.counter ++;
this.$emit('input',this.counter);
}
}
})
var app = new Vue({
el:'#app',
data:{
total:0
},
methods:{
handleGetTotal:function(total){
this.total = total;
}
}
})
</script>
v-model还可以用来创建自定义的表单输入组件,进行数据双向绑定,例如:
<div id="app">
<p>总数:{{total}}</p>
<my-component v-model="total"></my-component>
<button @click="handleReduce">-1</button>
</div>
<script>
Vue.component('my-component',{
props:['value'],
template:'<input :value="value" @input="updateValue">',
data:function(){
return {
counter:0
}
},
methods:{
updateValue:function(event){
this.$emit('input',event.target.value);
}
}
})
var app = new Vue({
el:'#app',
data:{
total:0
},
methods:{
handleReduce:function(){
this.total --;
}
}
})
</script>
实现这样一个具有双向绑定的v-model组件要满足下面两个要求:
- 接收一个value属性
- 在有新的value时触发input事件
18.在Vue.js 2.x中,推荐使用一个空的Vue实例作为中央事件总线(bus),也就是一个中介。示例代码:
<div id="app">
{{ message }}
<component-a></component-a>
</div>
<script>
var bus = new Vue();
Vue.component('component-a',{
template:'<button @click="handleEvent">传递事件</button>',
methods:{
handleEvent:function(){
bus.$emit('on-message','来自组件component-a的内容');
}
}
})
var app = new Vue({
el:'#app',
data:{
message:''
},
mounted:function(){
var _this = this;
//在实例初始化时,监听来自bus实例的事件
bus.$on('on-message',function(msg){
_this.message = msg;
})
}
})
</script>
首先创建了一个名为bus的空Vue实例。然后全局定义了组件component-a;最后创建Vue实例app,在app初始化时,也就是在生命周期mounted钩子函数里监听了来自bus的事件on-message,而在组件component-a中,点击按钮会通过bus把事件on-message发出去,此时app就会接收到来自bus的事件,进而在回调里完成自己的业务逻辑。
这种方法巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。如果深入使用,可以扩展bus实例,给它添加data、methods、computed等选项,这些都是可以公用的,在业务中,尤其是协同开发时非常有用,因为经常需要共享一些通用的信息,比如用户登录的昵称、性别、邮箱等,还有用户的授权token等。只需在初始化时让bus获取一次,任何时间,任何组件就可以从中直接使用了,在单页面富应用(SPA)中会很实用。项目比较大时,可以选择更好的状态惯例解决方案vuex。
19.除了中央事件总线bus外,还有两种方法可以实现组件间通信:父链 和子组件索引。
20.在子组件中,使用this.$parent 可以直接访问该组件的父实例或组件,父组件也可以通过this.$children 访问它的所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层的组件。示例:
<div id="app">
{{ message }}
<component-a></component-a>
</div>
<script>
var bus = new Vue();
Vue.component('component-a',{
template:'<button @click="handleEvent">通过父链直接修改数据</button>',
methods:{
handleEvent:function(){
//访问到父链后,可以做任何操作,比如直接修改数据
this.$parent.message = '来自组件component-a的内容';
}
}
})
var app = new Vue({
el:'#app',
data:{
message:''
}
})
</script>
尽管Vue允许这样操作,但在业务中,子组件应该尽可能地避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父组件紧耦合,只看父组件,很难理解父组件的状态,因为它可能被任意组件修改,理想情况下,只有组件自己能修改它的状态。父子组件最好还是通过props和$emit来通信。
21.当子组件较多时,通过this.$children 来一一遍历出我们需要的一个组件实例是比较困难的,尤其是组件动态渲染时,它们的序列是不固定的。
Vue提供了子组件索引的方法,用特殊的属性ref来为子组件指定一个索引名称,示例:
<div id="app">
<button @click="handleRef">通过ref获取子组件实例</button>
<component-a ref="comA"></component-a>
</div>
<script>
Vue.component('component-a',{
template:'<div>子组件</div>',
data:function(){
return {
message:'子组件内容'
}
}
})
var app = new Vue({
el:'#app',
methods:{
handleRef:function(){
//通过$refs来访问指定的实例
var msg = this.$refs.comA.message;
console.log(msg);
}
}
})
</script>
在父组件模版中,子组件标签上使用ref指定一个名称,并在父组件内通过this.$refs 来访问指定名称的子组件。$refs只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用$refs。
22.当需要让组件组合使用,混合父组件的内容和子组件的模板时,就会用到slot,这个过程叫作内容分发(transclusion)。以<app>为例,它有两个特点:
- <app>组件不知道它的挂载点会有什么内容。挂载点是由<app>的父组件决定的。
- <app>组件很可能有它自己的模板。
props传递数据、events触发事件和slot内容分发就构成了Vue组件的3个API来源,再复杂的组件也是由这3部分构成的。
-
slot。如下图,一个比较常规的网站布局:
这个网站由一级导航、二级导航、左侧列表、正文以及底部版权信息5个模块组成,如果要将它们都组件化,这个结构可能会是:
<app>
<menu-main></menu-main>
<menu-sub></menu-sub>
<div class="container">
<menu-left></menu-left>
<container></container>
</div>
</app>
当需要让组件组合使用,混合父组件的内容与子组件的模板时,就会用到slot,这个过程叫做内容分发(transclusion)。
以 <app>为例,它有两个特点:
- <app>组件不知道它的挂载点会有什么内容。挂载点的内容是由<app>的父组件决定的。
- <app>组件很可能有它自己的模板
props传递数据、events触发事件 和 slot 内容分发就构成了Vue组件的3个API来源,再复杂的组件也是由这3部分构成的。
编译的作用域。比如父组件有如下模板:
<child-component>
{{ message }}
</child-component>
这里的message就是一个slot,但是它绑定的是父组件的数据,而不是组件<child-component>的数据。
父组件模板的内容是在父组件作用域内编译,子组件模板的内容是在子组件作用域内编译。例如下面的示例:
<div id="app">
<child-component v-show="showChild"></child-component>
</div>
<script>
Vue.component('child-component',{
template:'<div>子组件</div>'
});
var app = new Vue({
el:'#app',
data:{
showChild:true
}
})
</script>
这里的状态showChild绑定的是父组件的数据,如果想在子组件上绑定,那应该是:
<div id="app">
<child-component></child-component>
</div>
<script>
Vue.component('child-component',{
template:'<div v-show="showChild">子组件</div>',
data:function(){
return {
showChild:true
}
}
});
var app = new Vue({
el:'#app'
})
</script>
因此,slot分发的内容,作用域是在父组件上的。
单个slot。在子组件内使用特殊的<slot>元素就可以为这个子组件开启一个slot(插槽),在父组件模板里,插入在子组件标签内的所有内容将替代子组件的<slot>标签及它的内容。示例代码:
<div id="app">
<child-component>
<p>分发的内容</p>
<p>更多分发的内容</p>
</child-component>
</div>
<script>
Vue.component('child-component',{
template:'\
<div>\
<slot>\
<p>如果父组件没有插入内容,我将作为默认出现</p>\
</solt>\
</div>'
});
var app = new Vue({
el:'#app'
})
</script>
子组件child-component的模板内定义了一个<slot>元素,并且用一个<p>作为默认的内容,在父组件没有使用slot时,会渲染这段默认的文本;如果写入了slot,那就会替换整个<slot>。所以上例渲染后的结果为:
<div id="app">
<div>
<p>分发的内容</p>
<p>更多分发的内容</p>
</div>
</div>
注意,子组件<slot>内的备用内容,它的作用域是子组件本身。
具名slot。给<slot>元素指定一个name后可以分发多个内容,具名slot可以与单个slot共存,例如:
<div id="app">
<child-component>
<h2 slot="header">标题</h2>
<p>正文内容</p>
<p>更多的正文内容</p>
<div slot="footer">底部信息</div>
</child-component>
</div>
<script>
Vue.component('child-component',{
template:'\
<div class="container">\
<div class="header">\
<slot name="header"></slot>\
</div>\
<div class="main">\
<slot></slot>\
</div>\
<div class="footer">\
<slot name="footer"></slot>\
</div>\
</div>'
});
var app = new Vue({
el:'#app'
})
</script>
子组件内声明了3个<slot>元素,其中在<div class="main">内的<slot>没有使用name特性,它将作为默认slot出现,父组件没有使用slot特性的元素与内容都将出现在这里。如果没有指定默认的匿名slot,父组件内多余的内容片段都将被抛弃。上例最终渲染结果为:
<div id="app">
<div class="container">
<div class="header">
<h2>标题</h2>
</div>
<div class="main">
<p>正文内容</p>
<p>更多的正文内容</p>
</div>
<div class="footer">
<div>底部信息</div>
</div>
</div>
</div>
在组合使用组件时,内容分发API至关重要。
作用域插槽。是一种特殊的slot。使用一个可以复用的模板替换已渲染元素。概念比较难理解,简单示例:
<div id="app">
<child-component>
<template scope="props">
<p>来自父组件的内容</p>
<p>{{props.msg}}</p>
</template>
</child-component>
</div>
<script>
Vue.component('child-component',{
template:'\
<div class="container">\
<slot msg="来自子组件的内容"></slot>\
</div>'
});
var app = new Vue({
el:'#app'
})
</script>
观察子组件的模板,在<slot>元素上有一个类似props传递数据给组件的写法 msg = "xxx",将数据传到了插槽。父组件中使用了<template>元素,而且拥有一个scope="props" 的特性,这里的props只是一个临时变量,就像v-for="item in items"里面的item 一样。template 内可以通过临时变量props访问来自子组件插槽的数据msg。
<div id="app">
<div class="container">
<p>来自父组件的内容</p>
<p>来自子组件的内容</p>
</div>
</div>
作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项。示例代码:
<div id="app">
<my-list :books="books">
<!--作用域插槽也可以是具名的slot-->
<template slot="book" scope="props">
<li>{{ props.bookName }}</li>
</template>
</my-list>
</div>
<script>
Vue.component('my-list',{
props:{
books:{
type:Array,
default:function(){
return [];
}
}
},
template:'\
<ul>\
<slot name="book"\
v-for="book in books"\
:book-name="book.name">\
<!--这里也可以写默认slot内容-->\
</slot>\
</ul>\
'
});
var app = new Vue({
el:'#app',
data:{
books:[
{ name:'《Vue.js实战》'},
{ name:'《JavaScript 语言精粹》'},
{ name:'《JavaScript 高级程序设计》'}
]
}
})
</script>
子组件my-list接收一个来自父级的prop数组books,并且将它在name为book的slot上使用v-for指令循环,同时暴露一个变量bookName。
作用域插槽的使用场景就是既可以复用子组件的slot,又可以使slot内容不一致。如果上例还在其他组件内使用,<li>的内容渲染权是由使用者掌握的,而数据却可以通过临时变量(比如props)从子组件内获取。
访问slot。Vue.js 2.x提供了用来访问被slot分发的内容的方法 $slots,示例:
<div id="app">
<child-component>
<h2 slot="header">标题</h2>
<p>正文内容</p>
<p>更多的正文内容</p>
<div slot="footer">底部信息</div>
</child-component>
</div>
<script>
Vue.component('child-component',{
template:'\
<div class="container">\
<div class="header">\
<slot name="header"></slot>\
</div>\
<div class="main">\
<slot></slot>\
</div>\
<div class="footer">\
<slot name="footer"></slot>\
</div>\
</div>',
mounted:function(){
var header = this.$slots.header;
var main = this.$slots.default;
var footer = this.$slots.footer;
console.log(footer)
console.log(footer[0].elm.innerHTML)
}
});
var app = new Vue({
el:'#app'
})
</script>
通过slots.default包括了所有没有被包含在具名slot中的节点。$slots在业务中几乎用不到,在用render函数创建组件时会比较有用,但主要还是用于独立组件开发中。
24.递归组件。组件在它的模板内可以递归地调用自己,只要给组件设置name的选项就可以了。示例代码:
<div id="app">
<child-component :count="1"></child-component>
</div>
<script>
Vue.component('child-component',{
name:'child-component',
props:{
count:{
type:Number,
default:1
}
},
template:'\
<div class="child">\
<child-component\
:count="count + 1"\
v-if="count < 3"></child-component>\
</div>'
})
var app = new Vue({
el:'#app'
})
</script>
设置name后,在组件模板内就可以递归使用了,不过需要注意,必须给一个条件来限制递归数量,否则会抛出错误:max stack size exceeded。
组件递归使用可以用来开发一些具有未知层级关系的独立组件,比如级联选择器和树形控件等。
25.内联模板。组件的模板一般都是在template选项内定义的。Vue提供了一个内联模板的功能,在使用组件时,给组件标签使用inline-template特性,组件就会把它的内容当作模板,而不是把它当内容分发,这让模板更灵活。示例:
<div id="app">
<child-component inline-template>
<div>
<h2>在父组件中定义子组件的模板</h2>
<p>{{message}}</p>
<p>{{msg}}</p>
</div>
</child-component>
</div>
<script>
Vue.component('child-component',{
data:function(){
return {
msg:'在子组件声明的数据'
}
}
});
var app = new Vue({
el:'#app',
data:{
message:'在父组件声明的数据'
}
})
</script>
渲染后的结果为:
<div id="app">
<div>
<h2>在父组件中定义子组件的模板</h2>
<p>在父组件声明的数据</p>
<p>在子组件声明的数据</p>
</div>
</div>
在父组件中声明的数据message和子组件中声明的数据msg,两个都可以渲染(如果同名,优先使用子组件的数据)。这反而是内联模板的缺点,就是作用域比较难理解,如果不是非常特殊的场景,建议不要轻易使用内联模板。
26.动态组件。Vue.js提供了一个特殊元素<component>用来动态地挂在不同的组件,使用is特性来选择要挂载的组件。示例:
<div id="app">
<component :is="currentView"></component>
<button @click="handleChangeView('A')">切换到A</button>
<button @click="handleChangeView('B')">切换到B</button>
<button @click="handleChangeView('C')">切换到C</button>
</div>
<script>
var app = new Vue({
el:'#app',
components:{
comA:{
template:'<div>组件A</div>'
},
comB:{
template:'<div>组件B</div>'
},
comC:{
template:'<div>组件C</div>'
}
},
data:{
currentView:'comA'
},
methods:{
handleChangeView:function(component){
this.currentView = 'com' + component;
}
}
})
</script>
动态地改变currentView的值就可以动态挂载组件了。也可以直接绑定在组件对象上:
<div id="app">
<component :is="currentView"></component>
</div>
<script>
var Home = {
template:'<p>Welcome home ! </p>'
}
var app = new Vue({
el:'#app',
data:{
currentView: Home
}
})
</script>
27.异步组件。当你的工程足够大,使用的组件足够多时,是时候考虑下性能问题了,因为一开始把所有的组件都加载是没有必要的一笔开销。好在Vue.js允许将组件定义为一个工厂函数,动态地解析组件。Vue.js只在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染。例如:
<div id="app">
<child-component></child-component>
</div>
<script>
Vue.component('child-component',function(resolve,reject){
window.setTimeout(function(){
resolve({
template:'<div>我是异步渲染的</div>'
});
},2000);
});
var app = new Vue({
el:'#app'
})
</script>
工厂函数接收一个resolve回调,在收到从服务器下载的组件定义时调用。也可以调用reject(reason)指示加载失败。这里setTimeout 指示为了演示异步,具体的下载逻辑可以自己决定,比如把组件配置携程一个对象配置,通过Ajax来请求,然后调用resolve传入配置选项。
28.$nextTick。场景描述:有一个div,默认用v-if将它隐藏,点击一个按钮后,改变v-if的值,让它显示出来,同时拿到这个div的文本内容。如果v-if的值是false,直接去获取div的内容是获取不到的,因为此时div还没有被创建出来,那么应该在点击按钮后,改变v-if的值为true,div才会被创建,此时再去获取,示例代码如下:
<div id="app">
<div id="div" v-if="showDiv">这是一段文本</div>
<button @click="getText">获取div内容</button>
</div>
<script>
var app = new Vue({
el:'#app',
data:{
showDiv:false
},
methods:{
getText:function(){
this.showDiv = true;
var text = document.getElementById('div').innerHTML;
console.log(text)
}
}
})
</script>
这段代码并不难理解,但是运行后在控制台会抛出一个错误:cannot read property 'innerHTML' of null, 意思是获取不到div元素。这就涉及Vue一个重要概念:异步更新队列。
Vue在观察到数据变化时并不是直接更新DOM,而是开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。在缓冲时会去除重复数据,从而避免不必要的计算和DOM操作。然后,在下一个事件循环tick中,Vue刷新队列并执行实际(已去重的)工作。所以如果你用一个for循环来动态改变是数据100次,其实它只会应用最后一次改变,如果没有这种机制,DOM就要重绘100次,这固然是一个很大的开销。
Vue会根据当前浏览器环境优先使用原生的Promise.then和MutationObserver,如果都不支持,就会采用setTimeout代替。
知道了Vue异步更新DOM的原理,上面的示例的报错也就不难理解了。
事实上,在执行this.showDiv=true;时,div仍然还是没有被创建出来,知道下一个Vue事件循环时,才开始创建。$nextTick就是用来知道什么时候DOM更新完成的,上面的示例改为:
<div id="app">
<div id="div" v-if="showDiv">这是一段文本</div>
<button @click="getText">获取div内容</button>
</div>
<script>
var app = new Vue({
el:'#app',
data:{
showDiv:false
},
methods:{
getText:function(){
this.showDiv = true;
this.$nextTick(function(){
var text = document.getElementById('div').innerHTML;
console.log(text)
})
}
}
})
</script>
这时在点击按钮,控制台就打印出div的内容“这是一段文本”了。
理论上,我们应该不用去主动操作DOM,因为Vue的核心思想就是数据驱动DOM,但在很多业务里,我们避免不了会使用一些第三方库,比如popper.js、swiper等,这些基于原生javascript的库都有创建和更新及销毁的完整生命周期,与Vue配合使用时,就要利用好$nextTick。
- X-Template。如果你没有使用webpack、gulp等工具,试想一下你的组件template的内容很冗长、复杂,如果都在JavaScript里拼接字符串,效率很低。Vue提供了另一种定义模板的方式,在<script>标签里使用 text/x-template类型,并且指定一个id,将这个id赋给template。示例:
<div id="app">
<my-component></my-component>
<script type="text/x-template" id="my-component">
<div> 这是组件的内容 </div>
</script>
</div>
<script>
Vue.component('my-component',{
template:'#my-component'
})
var app = new Vue({
el:'#app'
})
</script>
在<script>标签里,可以愉快地写HTML代码,不用考虑换行等问题。
Vue的初衷并不是滥用它,因为它将模板和组件的其他定义隔离了。
我们后续会使用webpack来编译.vue的单文件,从而优雅地解决HTML书写的问题。
30.手动挂在实例。我们现在所创建的实例都是通过new Vue()的形式创建出来的。在一些非常特殊的情况下,我们需要动态地去创建Vue实例,Vue提供了Vue.extend 和 mount()手动地挂载一个未挂载的实例。
这个方法返回实例自身,因而可以链式调用其他实例方法。示例:
<div id="mount-div">
</div>
<script>
var MyComponent = Vue.extend({
template:'<div>Hello:{{name}}</div>',
data:function(){
return {
name:'Aresn'
}
}
});
new MyComponent().$mount('#mount-div')
</script>
运行后,id为mount-div的div元素会被替换为组件MyComponent的template的内容:
<div>Hello:Aresn</div>
除了这种写法外,以下两种写法也是可以的:
new MyComponent({
el:'#mount-div'
});
//或者 ,在文档之外渲染并且随后挂载
var component = new MyComponent().$mount();
document.getElementById('mount-div').appendChild(component.$el);
手动挂载实例(组件)是一种比较极端的高级用法,在业务中几乎用不到,只在开发一些复杂的独立组件时可能会使用,所以只做了解就好。