組件是什么浸锨?
組件是可復(fù)用的 Vue 實例唇聘,且?guī)в幸粋€名字。
組件的注冊與使用
組件與vue實例一樣柱搜,需要注冊迟郎,才可以使用,注冊有全局注冊和局部注冊倆種方式
1聪蘸、全局注冊
<div id="app">
<my-component></my-component> // 使用組件
</div>
<script>
//全局注冊組件宪肖,必須在實例創(chuàng)建前注冊,注冊后健爬,任何vue實例都可以使用
Vue.component('my-component',{ // 'my-component' 就是組件的名字控乾,推薦寫法為小寫加減號分隔
template: '<p>我是組件的內(nèi)容</p>' // 組件的具體內(nèi)容,外層必須用一個標(biāo)簽包裹一下
})
new Vue({
el: '#app'
})
</script>
2娜遵、局部注冊
<div id="app">
<my-component></my-component>
</div>
<script>
new Vue({
el: '#app',
components: { // 在實例中蜕衡,使用components選項局部注冊,注冊后只有在該實例下有效
'my-component': {
template: '<p>這里是組件的內(nèi)容</p>'
}
}
})
</script>
以上代碼渲染后的結(jié)果都為:
<div id="app">
<p>我是組件的內(nèi)容</p>
</div>
3设拟、組件的嵌套
<div id="app">
<my-component></my-component>
</div>
<script>
new Vue({
el: '#app',
components: {
'my-component': {
template: '<p>hello <my-component2></my-component2></p>',
components: { // 在這里慨仿,可以繼續(xù)使用components選項注冊組件
'my-component2': {
template: '<span>tom</span>'
}
}
}
}
})
</script>
渲染后的結(jié)果為:
<p>
hello
<span>tom</span>
</p>
4、組件中定義數(shù)據(jù)
組件中纳胧,也可以使用vue實例中的那些選項镰吆,只是這里data的定義方式有一點點不同。
<div id="app">
<my-component></my-component>
</div>
<script>
var app = new Vue({
el: '#app',
components: {
'my-component': {
template: '<p>{{message}}</p>',
data: function () { // 定義一個函數(shù)跑慕,內(nèi)部返回一個對象万皿,對象中定義數(shù)據(jù)
return {
message: 'hello'
}
}
}
}
})
</script>
組件的通信
組件不僅僅是要把模板的內(nèi)容進行復(fù)用,更重要的是組件間要進行通信相赁。
1.1相寇、props——父組件向子組件傳遞數(shù)據(jù)
通常父組件的模板中包含子組件,父組件要正向的向子組件傳遞數(shù)據(jù)或者參數(shù)钮科,子組件接收到后根據(jù)參數(shù)的不同來渲染不同的內(nèi)容或者執(zhí)行操作唤衫,這個正向傳遞數(shù)據(jù)的過程就是通過props來實現(xiàn)的。
<div id="app">
<my-component :message="message"></my-component> // 使用 ':message'的形式傳遞父組件中的data绵脯,不加冒號佳励,就是傳遞字符串message
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'hello~'
},
components: {
'my-component': {
template: '<p>{{message}}</p>',
props: ['message'] // 使用props選項用數(shù)組(也可對象)的方式接收傳遞過來的數(shù)據(jù)休里,在template,computed赃承,method中都可使用
}
}
})
</script>
1.2妙黍、props的實際使用方式
通過props傳遞數(shù)據(jù)是單向的,也就是說瞧剖,父組件的數(shù)據(jù)變化了拭嫁,子組件跟著變化限佩,這沒問題脱惰,但是呢分尸,反過來蚤假,則不可以,vue不推薦直接修改prop的值(盡管可以實現(xiàn)子組件數(shù)據(jù)的變化)蔓彩,以下倆種使用方式比較常見峡钓。
1.2.1易迹、使用data保存
第一種就是父組件傳遞初始值進來巾遭,子組件將它作為初始值保存起來肉康,在自己的作用域下可以隨意使用和修改,這時可以在組件data內(nèi)再聲明一個數(shù)據(jù)灼舍,引用父組件的prop吼和。
<div id="app">
<my-component :message-par="message"></my-component>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'hello~'
},
components: {
'my-component': {
template: '<p><button @click="messageSon=\'hi\'">change</button>{{messageSon}}</p>', // 直接修改沒問題
props: ['messagePar'],
data: function () {
return {
messageSon: this.messagePar // 在data中定義一個數(shù)據(jù)引用prop
}
}
}
}
})
</script>
1.2.2、使用computed保存
這種情況就是prop作為需要被轉(zhuǎn)變的原始值傳入骑素,可以用計算屬性纹安。
<div id="app">
<my-component :width="100"></my-component>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'hello~'
},
components: {
'my-component': {
template: '<p :style="style">123</p>',
props: ['width'],
computed: {
style: function () {
return {
width: this.width + 'px'
}
}
}
}
}
})
</script>
1.3 prop的驗證
上面所說的props選項的值都是一個數(shù)組,其實當(dāng)prop需要驗證時砂豌,就需要使用對象寫法。一般光督,當(dāng)你的組件需要提供給別人使用時阳距,推薦都使用數(shù)據(jù)驗證,比如某個數(shù)據(jù)必須是數(shù)字類型结借,否則在控制臺彈出警告(必須引入的是vue包為開發(fā)版本)筐摘。
Vue.component('my-component',{
...,
props: {
propA: Number, // 必須是數(shù)字類型
propB: [String, Number], // 必須是字符串或者數(shù)字類型
propC: { // 必須是布爾值,如果沒傳的話默認(rèn)為true
type: Boolean,
default: true
},
propD: { // 必須是數(shù)字類型船老,且必傳
type: Number,
required: true
},
propE: { // 如果是數(shù)組或者對象咖熟,必須以一個函數(shù)來返回
type: Array,
default: function () {
return [];
}
},
propF: { // 自定義一個驗證函數(shù)
validator: function (value) {
return value > 10;
}
}
}
})
例子:
<div id="app">
<my-component :message="message"></my-component>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
message: 'hi~'
},
components: {
'my-component': {
template: '<p>{{message}}</p>',
props: {
message: Number // 定義了number類型
}
}
}
})
</script>
以上代碼雖然可以渲染,但會在控制臺報錯柳畔。
2.1馍管、$emit 子組件向父組件傳遞數(shù)據(jù)
props是不能把數(shù)據(jù)傳遞給父組件的,這里有一種方法薪韩,就是确沸,子組件用on()來監(jiān)聽子組件的事件。
<div id="app">
<p>{{total}}</p>
<my-component @increment="changeTotal" @reduce="changeTotal"></my-component> // 2罗捎、父組件根據(jù)子組件的事件名來接收观谦,并做出響應(yīng)
</div>
<script>
Vue.component('my-component',{
template: '\
<div>\
<button @click="handleIncrement">增加</button>\
<button @click="handleReduce">減少</button>\
</div>',
data: function(){
return {
counter: 0
}
},
methods: {
handleIncrement:function () {
this.counter ++;
this.$emit('increment', this.counter); // 1、觸發(fā)事件桨菜,并傳遞值 'increment'是定義事件的名稱
},
handleReduce:function () {
this.counter --;
this.$emit('reduce', this.counter);
}
}
})
var vm = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
changeTotal: function (value) { // 3豁状、具體響應(yīng),將子組件傳遞過來的值進行處理
this.total = value;
}
}
})
</script>
效果:
2.2倒得、$emit的語法糖
<div id="app">
<p>{{total}}</p>
<my-component v-model="total"></my-component> // 直接用v-model綁定一個父組件的數(shù)據(jù)
</div>
<script>
Vue.component('my-component',{
template: '\
<div>\
<button @click="handleReduce">減少</button>\
</div>',
data: function(){
return {
counter: 0
}
},
methods: {
handleReduce:function () {
this.counter --;
this.$emit('input', this.counter); // $emit()的事件名改為'input'
}
}
})
var vm = new Vue({
el: '#app',
data: {
total: 0
}
})
</script>
仍然是點擊減一的效果泻红,這里簡潔了許多。
2.3屎暇、v-model實現(xiàn)自定義的雙向綁定的表單輸入組件
<div id="app">
<p>{{total}}</p>
<my-component v-model="total"></my-component>
<button @click="total--">-1</button>
</div>
<script>
Vue.component('my-component',{
template: '<input type="text" :value="value" @input="handleInput" />',
props: ['value'],
methods: {
handleInput:function (event) {
this.$emit('input',event.target.value);
}
}
})
var vm = new Vue({
el: '#app',
data: {
total: 0
}
})
</script>
效果:
按鈕點擊承桥,和子組件文本框輸入都可以使父子組件的數(shù)據(jù)同時變化。
3.1根悼、中央事件總線(bus) —— 非父子組件的通信
這種方式巧妙而輕量的實現(xiàn)了任何組件間的通信凶异,包括父子,兄弟挤巡,跨級剩彬。比如兄弟組件間通信:
<div id="app">
<component-aaa></component-aaa>
<component-bbb></component-bbb>
</div>
<script>
var bus = new Vue(); // 1、首先創(chuàng)建一個空的vue實例
var vm = new Vue({
el: '#app',
components: {
'component-aaa': {
template: '<button @click="handleMessage">click me</button>',
data: function () {
return {
message: '我是來自子組件aaa的內(nèi)容~'
}
},
methods: {
handleMessage:function () {
bus.$emit('on-message',this.message); // 3矿卑、組件aaa中按鈕點擊把事件'on-message'發(fā)出去喉恋,同時攜帶相應(yīng)數(shù)據(jù)
}
}
},
'component-bbb':{
template: '<p>{{message}}</p>',
data: function(){
return {
message: '我是來自子組件bbb的內(nèi)容~'
}
},
mounted: function () {
var that = this;
bus.$on('on-message', function (value) { // 2、在組件bbb中監(jiān)聽來自bus的事件'on-message'
that.message = value;
})
}
}
}
})
</script>
4.1母廷、父鏈和子組件索引
除了中央事件總線bus外轻黑,還有倆種方法可以實現(xiàn)組件間通信:父鏈和子組件索引。
4.2琴昆、父鏈
在子組件中氓鄙,使用this.parent可以直接訪問該組件的父實例或組件,父組件也可以通過this.children訪問它所有的子組件业舍,而且可以遞歸向上或向下無限訪問抖拦。
<div id="app">
<p>{{message}}</p>
<my-component></my-component>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
message: 123
},
components: {
'my-component': {
template: '<button @click="handleMessage">click me</button>',
methods: {
handleMessage: function () {
this.$parent.message = 456; // 通過父鏈直接修改父組件的數(shù)據(jù)
}
}
}
}
})
</script>
還是推薦使用props或者$emit()的方式通信。
4.3舷暮、子組件索引
當(dāng)子組件較多時态罪,通過this.$children來一一遍歷出我們需要的一個組件實例是比較困難的,Vue提供了子組件索引的方法下面,用特殊的屬性ref來為子組件指定一個索引名稱:
<div id="app">
<button @click="handleMessage">click me</button>
<my-component ref="comA"></my-component> // 在父組件模板中子組件標(biāo)簽上复颈,使用ref指定一個名稱
</div>
<script>
var vm = new Vue({
el: '#app',
methods: {
handleMessage: function () {
this.$refs.comA.message = 456; // 父組件內(nèi)部,通過 this.$refs來訪問指定名稱的子組件的
}
},
components: {
'my-component': {
template: '<p>{{message}}</p>',
data: function () {
return {
message: 123
}
}
}
}
})
</script>
slot —— 分發(fā)內(nèi)容
1诸狭、單個slot
在子組件內(nèi)使用特殊的<slot>元素就可以為這個子組件開啟一個slot(插槽)券膀,在父組件模板里君纫,插入在子組件標(biāo)簽內(nèi)的所有內(nèi)容將替代子組件的<slot>標(biāo)簽及它的內(nèi)容。
<div id="app">
<my-component>
<p>hello</p>
<p>world</p>
</my-component>
</div>
<script>
Vue.component('my-component',{
template: '<div><slot><p>如果沒有內(nèi)容填充芹彬,我將作為默認(rèn)的內(nèi)容出現(xiàn)蓄髓。</p></slot></div>'
})
var vm = new Vue({
el: '#app'
})
</script>
// 渲染后
<div>
<p>hello</p>
<p>world</p>
</div>
子組件my-component的模板內(nèi)定義了一個slot元素,并且用一個<p>作為默認(rèn)的內(nèi)容舒帮,在父組件沒有使用slot時会喝,會渲染這段默認(rèn)的文本,如果寫入了slot玩郊,那就會替換整個<slot>肢执。
2、具名slot
給slot元素指定一個name后可以分發(fā)多個內(nèi)容译红,具名slot可以與單個slot共存:
<div id="app">
<my-component>
<p slot="header">我是頭部內(nèi)容</p>
<p>我是導(dǎo)航內(nèi)容</p>
<p>我是主體內(nèi)容</p>
<p slot="footer">我是底部內(nèi)容</p>
</my-component>
</div>
<script>
Vue.component('my-component',{
template: '\
<div>\
<div class="header"><slot name="header"></slot></div>\
<div class="main"><slot></slot></div>\
<div class="footer"><slot name="footer"></slot></div>\
</div>'
})
var vm = new Vue({
el: '#app'
})
</script>
// 渲染后
<div>
<div class="header">
<p>我是頭部內(nèi)容</p>
</div>
<div class="main">
<p>我是導(dǎo)航內(nèi)容</p>
<p>我是主體內(nèi)容</p>
</div>
<div class="footer">
<p>我是底部內(nèi)容</p>
</div>
</div>
子組件內(nèi)聲明了3個slot元素预茄,其中在<div class="main"></div>內(nèi)的slot沒有使用name特性,它將作為默認(rèn)slot出現(xiàn)侦厚,父組件沒有slot特性的元素與內(nèi)容都將出現(xiàn)在這里耻陕。如果沒有指定默認(rèn)的匿名slot元素,父組件內(nèi)多余的內(nèi)容片斷都將被拋棄刨沦。
3诗宣、作用域插槽
作用域插槽是一種特殊的slot,使用一個可以復(fù)用的模板替換已渲染元素想诅≌倥樱基本用法:
<div id="app">
<my-component>
<template scope="props">
<p>{{prop.message}}</p>
</template>
</my-component>
</div>
<script>
var vm = new Vue({
el: '#app',
components: {
'my-component': {
template: '<div><slot :message="message"></slot></div>',
data: function () {
return {
message: 123
}
}
}
}
})
</script>
// 渲染后
<div>
<p>123</p>
</div>
作用域插槽最具代表性的例子就是列表組件,比如:
<div id="app">
<my-component :fruits="fruits">
<template scope="props">
<li :style="{backgroundColor:(props.index%2===0?'skyblue':'pink')}">{{props.fruitName}}</li>
</template>
</my-component>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
fruits: ['香蕉','蘋果','梨子','西瓜','菠蘿','水蜜桃']
},
components: {
'my-component': {
template: '<ul><slot v-for="(item, index) in fruits" :fruitName="item" :index="index"></slot></ul>',
props: ['fruits']
}
}
})
</script>
效果:
作用域插槽的使用場景是既可以復(fù)用子組件的slot来破,又可以使用slot內(nèi)容不一致篮灼。
其他
1、$nextTick —— 異步更新隊列
現(xiàn)在有一個需求徘禁,有一個div 穿稳,默認(rèn)用v-if 將它隱藏,點擊一個按鈕后晌坤,改變v-if 的
值,讓它顯示出來旦袋,同時拿到這個div 的文本內(nèi)容骤菠。如果v-if 的值是false ,直接去獲取div 的內(nèi)容
是獲取不到的疤孕, 因為此時div 還沒有被創(chuàng)建出來商乎,那么應(yīng)該在點擊按鈕后,改變v -if 的值為true,
div 才會被創(chuàng)建祭阀,此時再去獲取鹉戚,示例代碼如下:
<div id="app">
<div id="con" v-if="flag">這是一段文本內(nèi)容</div>
<button @click="fn">click me</button>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
flag: false
},
methods: {
fn: function () {
this.flag = true;
var conText = document.getElementById("con").innerHTML;
console.log(conText);
}
}
})
</script>
然而鲜戒,在點擊按鈕之后,控制臺卻報錯了抹凳,Cannot read property 'innerHTML' of null遏餐,意思就是獲取不到div元素,這里就涉及Vue一個重要的概念赢底,異步更新隊列失都。
Vue 在觀察到數(shù)據(jù)變化時并不是直接更新DOM,而是開啟一個隊列幸冻,并緩沖在同一事件循環(huán)
中發(fā)生的所有數(shù)據(jù)改變粹庞。在緩沖時會去除重復(fù)數(shù)據(jù),從而避免不必要的計算和DOM 操作洽损。然后庞溜,
在下一個事件循環(huán)tick 中, Vue 刷新隊列井執(zhí)行實際(己去重的)工作碑定。所以如果你用一個for 循
環(huán)來動態(tài)改變數(shù)據(jù)100次流码,其實它只會應(yīng)用最后一次改變,如果沒有這種機制不傅, DOM 就要重繪100
次旅掂,這固然是一個很大的開銷。
知道了Vue 異步更新DOM 的原理访娶,上面示例的報錯也就不難理解了商虐。事實上,在執(zhí)行
this.flag = true時崖疤, div 仍然還是沒有被創(chuàng)建出來秘车,直到下一個Vue 事件循環(huán)時,才開始創(chuàng)建劫哼。
$nextTick 就是用來知道什么時候DOM 更新完成的叮趴,所以上面的示例代碼需要修改為:
<div id="app">
<div id="con" v-if="flag">這是一段文本內(nèi)容</div>
<button @click="fn">click me</button>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
flag: false
},
methods: {
fn: function () {
this.flag = true;
this.$nextTick(function () {
var conText = document.getElementById("con").innerHTML;
console.log(conText);
})
}
}
})
</script>
2、x-Templates
這個功能就是如果組件模板內(nèi)容太過于復(fù)雜权烧,不想拼接字符串眯亦,所以提供了一種方法,具體使用如下:
<div id="app">
<my-components></my-components>
</div>
<script type="text/x-template" id="my-components">
<div>
<p>123</p>
<p>456</p>
<p>789</p>
</div>
</script>
<script>
var vm = new Vue({
el: '#app',
components: {
'my-components': {
template: '#my-components'
}
}
})
</script>
// 渲染后
<div id="app">
<div>
<p>123</p>
<p>456</p>
<p>789</p>
</div>
</div>
3般码、watch —— 監(jiān)聽
watch選項用來監(jiān)聽某個prop或者data的改變妻率,當(dāng)它們發(fā)生改變時,就會觸發(fā)watch配置的函數(shù)板祝,從而完成我們的業(yè)務(wù)邏輯宫静。
<div id="app">
<p>{{message}}</p>
<button @click="fn">click me</button>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
message: 123
},
methods: {
fn: function () {
this.message = 456;
}
},
watch: {
// 監(jiān)聽了message,如果發(fā)生改變,就會觸發(fā)這個函數(shù)孤里,參數(shù)1為改變后的值伏伯,參數(shù)2為改變前的值
message: function (newValue,oddValue) {
console.log(newValue); // 456
console.log(oddValue); // 123
}
}
})
</script>
實戰(zhàn)
1、數(shù)字輸入框組件
<div id="app">
<input-number :max="10" :min="0" v-model="value"></input-number>
</div>
<script>
function isValueNumber(value){
return (/(^-?[0-9]+\.{1}\d+$)|(^-?[1-9][0-9]*$)|(^-?0{1}$)/).test(value);
}
Vue.component('input-number', {
template: '\
<div>\
<input type="text" :value="currentValue" @change="handleChange" />\
<button @click="handleDown">-1</button>\
<button @click="handleUp">+1</button>\
</div>',
props: {
max: {
type: Number,
default: Infinity
},
min: {
type: Number,
default: -Infinity
},
value: {
type: Number,
default: 0
}
},
data: function () {
return {
currentValue: this.value
}
},
methods: {
handleDown: function () {
if(this.currentValue<=this.min) return;
this.currentValue --;
},
handleUp: function () {
if(this.currentValue>=this.max) return;
this.currentValue ++;
},
handleChange: function (event) {
var val = event.target.value.trim();
if(isValueNumber(val)){
if(val>this.max){
this.currentValue = this.max;
}else if(val<this.min){
this.currentValue = this.min;
}else{
this.currentValue = val;
}
}else{
event.target.value = this.currentValue;
}
},
updateValue: function (val) {
if(val > this.max){
this.currentValue = this.max;
}else if(val < this.min){
this.currentValue = this.min;
}else{
this.currentValue = val;
}
}
},
watch: {
currentValue: function(val){
this.$emit('input',val);
},
value: function (val) {
this.updateValue(val);
}
},
mounted: function () {
this.updateValue(this.value);
}
})
var vm = new Vue({
el: '#app',
data: {
value: 5
},
methods: {
changeValue: function () {
this.value ++;
}
}
})
</script>