组件的注册
<body>
<div id="app1">
<h1>app1</h1>
<my-global-componet></my-global-componet>
<app1-my-component></app1-my-component>
<table>
<p>Vue组件的模板在某些情况下会受到HTML的限制,比如
table内规定只允许是tr、td、
th等这些表格元素,
所以在table内直接使用组件是无效的。这种情况下,可以使用特殊的is属性来挂载组件</p>
<tbody is="my-global-componet"></tbody>
</table>
</div>
<div id="app2">
<h1>app2</h1>
<my-global-componet></my-global-componet>
<app1-my-component></app1-my-component>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
//全局注册组件
Vue.component('my-global-componet', {
template: '<div>这里是全局组件的内容</div>'
});
var app1 = new Vue({
el: '#app1',
components: {
'app1-my-component': {
template: '<div>app1的局部组件,只能在 app1 中使用</div>'
}
},
data: {
}
})
var app2 = new Vue({
el: '#app2',
data: {
}
})
</script>
</body>
-
全局注册组件
Vue.component()
-
局部组件:
new Vue({components:...})
-
is="my-global-componet"
Vue组件的模板在某些情况下会受到HTML的限制,比如table内规定只允许是tr、td、 th等这些表格元素,所以在table内直接使用组件是无效的。这种情况下,可以使用特殊的is属性来挂载组件
组件的选项
data
组件的data必须是函数,然后将数据return出去
<body>
<div id="app">
<my-global-componet></my-global-componet>
<my-global-componet></my-global-componet>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-global-componet', {
template: '<div>
组件的data.message:{{message}}
<button @click="count++">组件的data.count:{{ count }}</button>
<button @click="count++">组件的data.count:{{ count }}</button>
</div>',
data: function () {
//组件的data必须是函数,然后将数据return出去,
return {
message: '组件的内容!!!',
count: 0
}
}
});
var app = new Vue({
el: '#app'
})
</script>
</body>
使用props传递数据
props传递数据之数组
<body>
<div id="app">
<h1>组件</h1>
<my-componet message="来自父组件的数据1" warming-text="来自父组件的数据2" :pare-msg="parentDynamicMsg">
</my-componet>
<h1>父页面</h1>
<input type="text" v-model="parentDynamicMsg">
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-componet', {
template: '<div>
<p>message:{{message}}</p>
<p>warmingText:{{warmingText}}</p>
<p>pareMsg:{{pareMsg}}</p>
<div>',
props: ['message', 'warmingText', 'pareMsg'] //驼峰命名(camelCase)的props名称要转为短横分隔命名
});
var app = new Vue({
el: '#app',
data: {
parentDynamicMsg: "来自父组件的动态数据"
}
})
</script>
</body>
- 使用【字符串数组】来定义组件的一系列属性:
props: ['message', 'warmingText', 'pareMsg']
- 属性命名规则:驼峰命名(camelCase)的
props
名称要转为短横分隔命名 - 使用
v-bing:
绑定来自父组件的动态数据,:pare-msg="parentDynamicMsg"
props传递数据之对象
参见 数据验证
单向数据流
Vue 2.x与Vue 1.x比较大的一个改变就是,Vue 2.x通过props传递数据是单向的了,也就是父组件数据变化时会传递给子组件,但是反过来不行。而在Vue 1.x里提供了.sync修饰符来支持双向绑定。
<body>
<div id="app">
<h1>组件</h1>
<my-componet :pare-msg="parentDynamicMsg">
</my-componet>
<h1>父页面</h1>
<input type="text" v-model="parentDynamicMsg">
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-componet', {
template: '<div>
<p>pareMsg:{{pareMsg}}</p>
<input type="text" v-model="pareMsg">
</div>',
props: ['pareMsg']
});
var app = new Vue({
el: '#app',
data: {
parentDynamicMsg: "来自父组件的动态数据"
}
})
</script>
</body>
在组件中修改pareMsg
的值并不会影响到 父组件parentDynamicMsg
的值
如何使组件的值独立
<body>
<div id="app">
<h1>组件</h1>
<my-componet :pare-msg="parentDynamicMsg">
</my-componet>
<h1>父页面</h1>
<input type="text" v-model="parentDynamicMsg">
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-componet', {
template: '<div>
<p>myComponetMessage:{{myComponetMessage}}</p>
<input type="text" v-model="myComponetMessage">
</div>',
props: ['pareMsg'],
data: function () {
return {
myComponetMessage: this.pareMsg //之后myComponetMessage就父组件的parentDynamicMsg无关,两者互不影响
}
}
});
var app = new Vue({
el: '#app',
data: {
parentDynamicMsg: "来自父组件的动态数据"
}
})
</script>
</body>
在组件的data
定义一个变量myComponetMessage
引用属性pareMsg
,之后myComponetMessage
就父组件的parentDynamicMsg
无关,两者互不影响
组件的计算属性
<body>
<div id="app">
<h1>组件</h1>
<my-componet :my-width="componentWidth">
</my-componet>
<h1>父页面</h1>
<input type="text" v-model="componentWidth" placeholder="请输入宽度">
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-componet', {
template: '<div>
<input type="text" :style="styles" :value="myWidth"/>
</div>',
props: ['myWidth'],
computed: {
styles: function () {
return {
color: 'red',
this.myWidth + 'px'
}
}
}
});
var app = new Vue({
el: '#app',
data: {
componentWidth: 100
}
})
</script>
</body>
父组件的componentWidth
无单位px
,在组件中通过计算属性styles.
width 把单位px
补上
数据验证
我们上面所介绍的props选项的值都是一个数组,一开始也介绍过,除了数组外,还可以是对象,当prop需要验证时,就需要对象写法。
一般当你的组件需要提供给别人使用时,推荐都进行数据验证,比如某个数据必须是数字类型,如果传入字符串,就会在控制台弹出警告。
(示例略)
组件数据传递之-对象
<body>
<div id="app">
<my-list :cpt-books="mybooks" :cpt-msg="myMsg"></my-list>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-list', {
template: '<div>
<h1>cptMsg={{cptMsg}}</h1>
<ul>
<li v-for="book in cptBooks">{{book.name}}</li>
</ul>
</div>',
props: {
cptMsg: {
type: String,
default: function () {
return "--";
}
},
cptBooks: {
type: Array,
default: function () {
return [];
}
}
}
});
var app = new Vue({
el: '#app',
data: {
myMsg: "来自父组件的消息",
mybooks: [
{ name: '《Vue.js实战》' },
{ name: '《JavaScript语言精粹》' },
{ name: '《JavaScript高级程序设计》' }
]
}
})
</script>
</body>
组件通信
从父组件向子组件通信,通过props传递数据就可以了,但Vue组件通信的场景不止有这一种,
组件关系可分为
- 父子组件通信、
- 兄弟组件通信、
- 跨级组件通信。
本节将介绍各种组件之间通信的方法。
自定义事件
当子组件需要向父组件传递数据时,就要用到自定义事件。我们在介绍指令v-on时有提到,v-on除了监听DOM事件外,还可以用于组件之间的自定义事件。
-
子组件用$emit()来触发事件,
-
父组件用$on()来监听子组件的事件。
父组件也可以直接在子组件的自定义标签上使用v-on来监听子组件触发的自定义事件,
<body>
<div id="app">
<h1>组件</h1>
<my-componet @increase="handleGetTotal" @reduce="handleGetTotal">
</my-componet>
<h1>父组件</h1>
<p>total={{total}}</p>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-componet', {
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>
</body>
- 发起自定义事件,参数可以是若干个
this.$emit('increase', this.counter);
子组件向父组件传递数据-使用v-model和input事件名
<body>
<div id="app">
<h1>组件</h1>
<my-componet v-model="total">
</my-componet>
<h1>父组件</h1>
<p>total={{total}}</p>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-componet', {
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('input', this.counter);
},
handleReduce: function () {
this.counter--;
this.$emit('input', this.counter);
},
},
});
var app = new Vue({
el: '#app',
data: {
total: 0
}
})
</script>
</body>
子组件向父组件传递数据-使用v-model时,事件名必须是input
任何组件传递数据
在Vue.js 2.x中,推荐使用一个空的Vue实例作为中央事件总线(bus),也就是一个中介。
<body>
<div id="app">
<h1>组件</h1>
<my-componet></my-componet>
<h1>父组件</h1>
<p>total={{total}}</p>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
//1.使用一个空的Vue实例作为中央事件总线(bus)
var bus = new Vue();
Vue.component('my-componet', {
template: '<div>
<button @click="handleIncrease">+1</button>
<button @click="handleReduce">-1</button>
</div>',
data: function () {
return {
counter: 0
}
},
methods: {
handleIncrease: function () {
this.counter++;
//2.bus发起事件
bus.$emit('on-counter-change', this.counter);
},
handleReduce: function () {
this.counter--;
//2.bus发起事件
bus.$emit('on-counter-change', this.counter);
},
},
});
var app = new Vue({
el: '#app',
data: {
total: 0
}, mounted: function () {
let _this = this;
//3.bus监听事件
bus.$on('on-counter-change', function (counter) {
_this.total = counter;
})
}
})
</script>
</body>
首先创建了一个名为bus的空Vue实例,里面没有任何内容;
组件间通信-父链
在子组件中,使用this.$parent可以直接访问该组件的父实例或组件,父组件也可以通过this.$children访问它所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层的组件
<body>
<div id="app">
<h1>组件</h1>
<my-componet></my-componet>
<h1>父组件</h1>
<p>total={{total}}</p>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-componet', {
template: '<div>
<button @click="handleIncrease">+1</button>
<button @click="handleReduce">-1</button>
</div>',
data: function () {
return {
counter: 0
}
},
methods: {
handleIncrease: function () {
this.$parent.total++;
},
handleReduce: function () {
this.$parent.total++;
},
},
});
var app = new Vue({
el: '#app',
data: {
total: 0
}
})
</script>
</body>
尽管Vue允许这样操作,但在业务中,子组件应该尽可能地避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父子组件紧耦合,只看父组件,很难理解父组件的状态,因为它可能被任意组件修改,理想情况下,只有组件自己能修改它的状态。父子组件最好还是通过props和$emit来通信。
组件间通信-组件索引
当子组件较多时,通过this.$children来一一遍历出我们需要的一个组件实例是比较困难的,尤其是组件动态渲染时,它们的序列是不固定的。Vue提供了子组件索引的方法,用特殊的属性ref来为子组件指定一个索引名称
<body>
<div id="app">
<h1>组件</h1>
<my-componet ref="refMyComponent"></my-componet>
<h1>父组件</h1>
<button @click="handleIncrease">+1</button>
<button @click="handleReduce">-1</button>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-componet', {
template: '<div>
counter={{counter}}
</div>',
data: function () {
return {
counter: 0
}
}
});
var app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleIncrease: function () {
this.$refs.refMyComponent.counter++;
},
handleReduce: function () {
this.$refs.refMyComponent.counter--
},
}
})
</script>
</body>
<my-componet ref="refMyComponent"></my-componet>
this.$refs.refMyComponent.counter++;
$refs只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用$refs。
什么是slot
当需要让组件组合使用,混合父组件的内容与子组件的模板时,就会用到slot,这个过程叫作内容分发(transclusion)。以<app>
为例,它有两个特点:<app>
组件不知道它的挂载点会有什么内容。挂载点的内容是由<app>
的父组件决定的。<app>
组件很可能有它自己的模板。
props传递数据、events触发事件和slot内容分发就构成了Vue组件的3个API来源,再复杂的组件也是由这3部分构成的。
单个slot
在子组件内使用特殊的<slot>
元素就可以为这个子组件开启一个slot(插槽),在父组件模板里,插入在子组件标签内的所有内容将替代子组件的<slot>
标签及它的内容。
<body>
<div id="app">
<my-componet></my-componet>
<my-componet>
<p>父子组件内容</p>
</my-componet>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-componet', {
template: '<div>
<slot>如果父组件没有插入内容,我将作为默认出现</slot>
</div>'
});
var app = new Vue({
el: '#app'
})
</script>
</body>
具名slot
给<slot>
元素指定一个name后可以分发多个内容,具名Slot可以与单个Slot共存
<body>
<div id="app">
<my-componet>
<h2 slot="header">标题</h2>
<p>正文内容</p>
<p>更多的正文内容</p>
<div slot="footer">底部信息</div>
</my-componet>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-componet', {
template: '<div class="container">
<div class="header">
<slot name="header">slot中的默认标题</slot>
</div>
<div class="main">
<slot></slot>
</div>
<div class="main2">
<slot></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>'
});
var app = new Vue({
el: '#app'
})
</script>
</body>
html最后被解析为
<div id="app">
<div class="container">
<div class="header">
<h2>标题</h2>
</div>
<div class="main">
<p>正文内容</p>
<p>更多的正文内容</p>
</div>
<div class="main2">
<p>正文内容</p>
<p>更多的正文内容</p>
</div>
<div class="footer">
<div>底部信息</div>
</div>
</div>
</div>
<slot>
没有使用name特性,它将作为默认slot出现,父组件没有使用slot特性的元素与内容都将出现在这里。
如果没有指定默认的匿名slot,父组件内多余的内容片段都将被抛弃。
作用域插槽
作用域插槽是一种特殊的slot,使用一个可以复用的模板替换已渲染元素。
<body>
<div id="app">
<my-componet>
<template slot-scope="props">
<p>来自父组件的内容</p>
<p>{{props.msg}}</p>
</template>
</my-componet>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-componet', {
template: '<div class="container">
<slot msg="来自子组件的内容"></slot>
</div>'
});
var app = new Vue({
el: '#app'
})
</script>
</body>
scope="this api replaced by slot-scope in 2.5.0+"
,使用slot-scope
观察子组件的模板,在<slot>
元素上有一个类似props传递数据给组件的写法msg="xxx"
,将数据传到了插槽。父组件中使用了<template>
元素,而且拥有一个scope="props"的特性,这里的props 只是一个临时变量,就像v-for="item in items"里面的item一样。template内可以通过临时变量 props 访问来自子组件插槽的数据msg。
- 作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项。
<body>
<div id="app">
<my-list :cptbooks="mybooks">
<!-- 作用域插槽也可以是具名的Slot -->
<template slot="book" slot-scope="props">
<li>{{ props.bookName }}</li>
</template>
</my-list>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-list', {
props: {
cptbooks: {
type: Array,
default: function () {
return [];
}
}
},
template: '
<ul>
<slot name="book"
v-for="book in cptbooks"
:book-name="book.name">
<!-- 这里也可以写默认 slot内容 -->
</slot>
</ul> '
});
var app = new Vue({
el: '#app',
data: {
mybooks: [
{ name: '《Vue.js实战》' },
{ name: '《JavaScript语言精粹》' },
{ name: '《JavaScript高级程序设计》' }
]
}
})
</script>
</body>
访问slot
Vue.js 2.x提供了用来访问被slot分发的内容的方法·$slots
<body>
<div id="app">
<my-componet>
<h2 slot="header">标题</h2>
<p>正文内容</p>
<p>更多的正文内容</p>
<div slot="footer">底部信息</div>
</my-componet>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-componet', {
template: '<div class="container">
<div class="header">
<slot name="header">slot中的默认标题</slot>
</div>
<div class="main">
<slot></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>',
mounted: function () {
/*
使用$slot.访问slot
*/
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>
</body>
通过$slots可以访问某个具名slot,this.$slots.default包括了所有没有被包含在具名slot中的节点。尝试编写代码,查看两个console 打印的内容。
$slots在业务中几乎用不到,在用render函数(进阶篇中将介绍)创建组件时会比较有用,但主要还是用于独立组件开发中。
组件高级用法
递归组件
组件在它的模板内可以递归地调用自己,只要给组件设置name的选项就可以了
设置name后,在组件模板内就可以递归使用了,不过需要注意的是,必须给一个条件来限制递归数量,否则会抛出错误:max stack size exceeded。
<body>
<div id="app">
<my-component :count="appAcount"></my-component>
<button @click="add()">+1</button>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-component', {
name: "my-component",
template: '
<div>
{{count}}
<my-component :count="count + 1" v-if="count < 5"> </my-component>
</div> '
, props: {
count: {
type: Number,
default: 0
}
}
});
var app = new Vue({
el: '#app',
data: {
appAcount: 1
}, methods: {
add: function () {
this.appAcount++;
}
},
})
</script>
</body>
组件递归使用可以用来开发一些具有未知层级关系的独立组件,比如级联选择器和树形控件等
内联模板
<body>
<div id="app">
<child-component inline-template>
<div>
<h2>在父组件中定义子组件的模板</h2>
<p>{{ message }}</p>
<p>{{ msg }}</p>
</div>
</child-component>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('child-component', {
data: function () {
return {
msg: '在子组件声明的数据'
}
}
});
var app = new Vue({
el: '#app',
data: {
message: '在父组件声明的数据'
}
});
</script>
</body>
组件的模板一般都是在template选项内定义的,Vue提供了一个内联模板的功能,在使用组件时,给组件标签使用inline-template特性,组件就会把它的内容当作模板,而不是把它当内容分发,这让模板更灵活。
在父组件中声明的数据message和子组件中声明的数据msg,两个都可以渲染(如果同名,优先使用子组件的数据)。这反而是内联模板的缺点,就是作用域比较难理解,如果不是非常特殊的场景,建议不要轻易使用内联模板。
实际报错:
vue.2.6.11.js:634 [Vue warn]: Property or method "message" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.
无法识别父组件中的message
变量
动态组件
Vue.js提供了一个特殊的元素<component>
用来动态地挂载不同的组件,使用is特性来选择要挂载的组件。
<body>
<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>
<button @click="handleChangeView('D')">切换到D</button>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('global-component', {
template: '<div>组件D</div>'
});
var app = new Vue({
el: '#app',
data: {
currentView: 'comA'
},
components: {
comA: {
template: '<div>组件A</div>'
},
comB: {
template: '<div>组件B</div>'
},
comC: {
template: '<div>组件C</div>'
}
},
methods: {
handleChangeView: function (component) {
if (component === "D") {
this.currentView = 'global-component';
} else {
this.currentView = 'com' + component;
}
}
}
});
</script>
</body>
<component :is="currentView"></component>
异步组件
使用的组件足够多时,是时候考虑下性能问题了,因为一开始把所有的组件都加载是没必要的一笔开销。好在Vue.js允许将组件定义为一个工厂函数,动态地解析组件。Vue.js只在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染。
<body>
<div id="app">
<my-component></my-component>
</div>
<script src="../lib/vue.2.6.11.js"></script>
<script>
Vue.component('my-component', function (resolve, reject) {
setTimeout(function () {
resolve({
template: '<div>异步组件</div>'
});
}, 2000)
});
var app = new Vue({
el: '#app'
});
</script>
</body>
工厂函数接收一个resolve回调,在收到从服务器下载的组件定义时调用。也可以调用reject(reason)指示加载失败。这里setTimeout只是为了演示异步,具体的下载逻辑可以自己决定,比如把组件配置写成一个对象配置,通过Ajax来请求,然后调用resolve传入配置选项。