- 组件通信综述
- 子组件向父组件传递数据:v-on监听自定义事件
- 子组件向父组件传递数据:v-model绑定
- 非父子组件通信
- 使用slot
- 父组件向子组件传递数据:props使用字符串数组
- 作用域插槽
- 访问slot
- 组件高级用法
- 异步更新DOM后执行代码
- X-Template
- 手动挂载实例
组件通信
父组件单向将数据传递给子组件只是通信的一种方式。实际上组件通信可以分为:
- 父子组件通信
- 兄弟组件通信
- 跨级组件通信
这些还都分为单向和双向。在组件中学习的props只是父组件传数据给子组件的方式。还有很多传递数据的方式。
子组件向父组件传递数据
Vue提供了子组件用$emit()
来触发事件,父组件$on()
来监听子组件的事件。
在实际开发中,可以在子组件的标签上使用v-on来监听子组件触发的自定义事件
<div id="app">
<p>传给子组件的是{{count}}</p>
<my-comp :child-count="count" @event-from-child="fatherFunction"></my-comp>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
props:["childCount"],
template: "<div>" +
"<button @click='myFunction1'>++</button>" +
"<button @click='myFunction2'>--</button></div>",
methods: {
myFunction1: function () {
this.initCount++;
this.$emit('event-from-child', this.initCount);
},
myFunction2: function () {
this.initCount--;
this.$emit('event-from-child', this.initCount);
}
},
data: function () {
return {
initCount: this.childCount
}
}
}
},
data: {
message: [1, 2, 3, 4, 5],
count: 0
},
methods: {
fatherFunction(value) {
console.log("从子组件传来的数据是:" + value);
}
}
})
</script>
注意这里红色的几行。先看子组件,子组件从父组件接收了count属性,并用一个initCount存放了这个属性。这样做到数据和父组件隔离。
此后子组件两个按钮定义了两个事件,一个减少自己的initCount,一个增加自己的initCount,然后使用了特殊的$emit(自定义事件名称,数据)
方法来生成一个自定义事件。
父组件将这个自定义事件的名称写在子组件的元素标签内,对其监听并用自己的fatherFunction
去处理。
fatherFunction
参数直接就是子组件传递来的数据。子组件的$emit
可以传递多个数据参数,在父组件的事件内也提供同样多的参数进行接收即可。
通过自定义事件,就可以实现子组件向父组件传递数据。
除了直接监听自定义事件之外,还可以监听原生的DOM事件,在上边的例子里如果还想监听子组件内所有的按钮被按下的事件,可以增加如下代码:
<div id="app">
<p>传给子组件的是{{count}}</p>
<my-comp :child-count="count" @event-from-child="fatherFunction" @click.native="clickhandler"></my-comp>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
props: ["childCount"],
template: "<div>" +
"<button @click='myFunction1'>++</button>" +
"<button @click='myFunction2'>--</button></div>",
methods: {
myFunction1: function () {
console.log(this.initCount);
this.$emit('event-from-child', this.initCount, this.initCount + 10, this.initCount + 100);
this.initCount++;
},
myFunction2: function () {
console.log(this.initCount);
this.$emit('event-from-child', this.initCount, this.initCount + 10, this.initCount + 100);
this.initCount--;
}
},
data: function () {
return {
initCount: this.childCount
}
}
}
},
data: {
message: [1, 2, 3, 4, 5],
count: 0
},
methods: {
fatherFunction(value, value1, value2) {
console.log("从子组件传来的数据是:" + value);
console.log("从子组件传来的数据是:" + value1);
console.log("从子组件传来的数据是:" + value2);
},
clickhandler:function () {
console.log("我被按了");
}
}
})
</script>
v-model监听事件
v-model其实是一个语法糖,会自动监听input事件并从中获得数据,本质上还是监听自定义事件。看一个简单的例子
<div id="app">
<p v-if="total">从子组件传来的数据是{{total}}</p>
<my-comp v-model="total"></my-comp>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
template: "<div><button @click='myFunction'>++</button></div>",
methods: {
myFunction: function () {
this.count++;
this.$emit('input', this.count);
},
},
data: function () {
return {
count: 0
}
}
}
},
data: {
total: 0
},
methods: {
fatherFunction(value) {
this.total = value;
console.log("从子组件传来的数据是:" + value);
},
}
})
</script>
这个东西其实就是一个语法糖,需要使用v-model语法糖,需要满足两个条件:
- v-model在子组件元素上绑定父组件一个data属性
- 子组件必须使用
$emit
产生名称为input
的事件并传递一个value值
有了父子组件通信之后,很多想法就可以实现了,比如组件是一个表单,接受输入之后传给父组件。
非父子组件通信
很多时候可能还需要非父子组件之间通信,比如两个各渲染一块内容的组件通信。
这个时候推荐用一个专门的Vue实例,来做事件中介。在进阶的时候再来学习这个。
现在先来看两种方式:
- 父链
- 子组件索引
父链其实就是在组件中去直接访问父级组件和子组件,采用this.$parent
就可以访问当前组件的父级组件,this.$children
则可以访问子组件。
可以一层一层向上或者向下访问,可以取得对组件的完全控制。
尽管父链和子链是Vue提供的功能,但实际中最好不要去写直接操作父子组件的代码,这样会造成高耦合,如果是单纯的父子组件,最好还是使用之前介绍的通信方法。
一个父组件有多个子组件是很常见的,如果通过this.$children
遍历,由于渲染顺序不固定,比较麻烦。Vue提供了子组件索引的方法,用ref
属性再子组件标签上为子组件指定一个索引名称,之后就可以比较方便的通过this.$refs.xxx
访问该子组件。
<div id="app">
<button @click="clickhandler">从子组件获取消息</button>
<my-comp ref="child"></my-comp>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
template: "<div><button>++</button></div>",
data: function () {
return {
count: 0,
message: "子组件的内容"
}
}
}
},
data: {
total: 0,
message:""
},
methods: {
clickhandler() {
this.message = this.$refs.child.message;
console.log("从子组件传来的数据是:" + this.message);
},
}
})
</script>
使用单个slot
有的时候需要混合父组件与子组件的模板,就会用到slot,就是内容分发。
用一个形象的说明就是,一个子组件模板的一块区域是留给父组件的,像一个插槽一样,其实际渲染的部分,是父组件渲染出来的。
一个Vue组件对外交互的API,其实就是三块内容,之前学过了props,还有事件传递,现在还有第三个,就是slot。
在子组件内使用特殊的<slot>
就可以为这个组件开启一个插槽,父组件可以把渲染的内容插进来。
<div id="app">
<my-comp>
<p>{{fathermessage}}</p>
</my-comp>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
template: "<div><slot><p>子组件的默认内容</p></slot></div>",
data: function () {
return {
count: 0,
}
}
}
},
data: {
fathermessage: "父组件的信息",
}
})
</script>
在子组件元素中的内容,也就是<p>{{fathermessage}}</p>
,是父组件插入给子组件的内容,注意这里的作用域是父组件而不是子组件。
在子组件的slot标签中的HTML元素,是父组件没有插入内容的时候,默认显示的内容。slot标签中的内容是子组件的作用域。
这个例子渲染之后的实际结果是父组件的信息,如果删除<p>{{fathermessage}}</p>
,则会显示子组件的默认内容。
具名slot
在父组件的渲染区域,可以给元素上添加slot属性,用于指定一个命名的插槽。在组件内,则要通过slot标签的name属性来指定对应名称的插槽。
指定了名称之后,父组件渲染的元素就会插到子组件同名的插槽中。如果子组件有一个不带有name属性的slot标签,则父元素所有不具名的插入元素都会被收集到这个无名的slot标签中。
<div id="app">
<my-comp>
<p slot="slot1">这是插入给slot1插槽的内容</p>
<p slot="slot2">这是插入给slot2插槽的内容</p>
<p>这是父元素的不具名插槽的内容</p>
</my-comp>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
template: "<div><slot name='slot2'></slot><slot name='slot1'></slot><slot></slot></div>",
data: function () {
return {
count: 0,
message: "子组件的内容"
}
}
}
},
data: {
total: 0,
fathermessage: "父组件的信息",
message: "",
}
})
</script>
可以看到父组件渲染的内容分别插入到了不同的插槽中,通过结果页面的顺序就可以看到。如果没有匿名的slot,则父组件所有不具名的slot渲染内容都会被抛弃。
可以看到内容分发API也是非常重要的。
作用域插槽
作用域插槽使用一个模板替换已经渲染的元素,先看一个例子:
<div id="app">
<my-comp>
<template slot-scope="props">
<p>父组件渲染的内容</p>
<p>{{props.msg}}</p>
</template>
</my-comp>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
template: "<div><slot msg='子组件渲染的内容'></slot></div>",
}
}
})
</script>
这个作用域插槽在Vue 2.5之后,要使用slot-scope
属性,这个属性的值是一个临时变量。
在template之内的父组件的渲染内容里,访问这个临时变量中,子组件在插槽上设置的同名属性,即可渲染成具体的值。
这个作用域插槽的意义何在呢?实际上可以把数据集中存放在父组件中,子组件只负责具体渲染。看一个常用的列表的例子:
<div id="app">
<my-list :books="books">
<!-- 作用域插槽具名slot-->
<template slot="book" slot-scope="props">
<p>父组件自己想插入到每个插槽的内容</p>
<!-- 插入到子组件插槽中的li元素+子组件的书名的内容-->
<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></ul>'
});
var app = new Vue({
el: "#app",
data: {
books: [
{
name: "Vue实战",
},
{
name: "JS Promise",
},
{
name: "TypeScript实战",
},
]
}
})
</script>
在这个例子里,父子组件先通过props,父组件把books对象数组传递给子组件。子组件拿到了books对象数组后,实际上在模板里,使用v-for弄出了一堆slot,然后分别将每个slot的book-name属性(注意HTML标签和驼峰对应关系)设置上。父组件在template中插入插槽的是li加上每个插槽的book-name属性的渲染内容。
通过渲染结果可以看到,父组件的slot插入到了子组件的每一个同名的插槽之中。这实际上是一对多的分发,很有意思。
如果采用之前的例子,直接在父组件中使用v-for,则父组件内既有数据又负责渲染。有了插槽之后,结合props,父组件可以做到只分发数据而且控制渲染的位置,子组件负责具体渲染,有效解耦。
作用域插槽的使用场景就是既可以复用子组件的slot ,又可以使slot内容不一致。如果上例还在其他组件内使用,li标签的内容渲染权是由使用者掌握的,而数据却可以通过临时变量(比如props)从子组件内获取。
访问slot
之前都是将内容分发到slot中进行渲染,如果想要便捷的获取渲染后的slot对应的元素,可以使用$slots.slotName
来获取。不具名的slot对象都会存放在slot.default
中。
<div id="app">
<my-list>
<template slot="slot1">
<p>第一个插槽内容</p>
</template>
<template slot="slot2">
<p>第二个插槽的内容</p>
</template>
<template>
<p>不具名插槽的内容</p>
</template>
</my-list>
</div>
<script>
Vue.component('my-list', {
props: {
books: {
type: Array,
default: function () {
return [];
}
}
},
template: '<p>' +
'<slot name="slot1" >11</slot>' +
'<slot name="slot2" >22</slot>' +
'<slot>33</slot>' +
'</p>',
mounted:function () {
var slot1 = this.$slots.slot1;
var slot2 = this.$slots.slot2;
var nonameslot = this.$slots.default;
console.log(slot1);
console.log(slot2);
console.log(nonameslot);
}
});
var app = new Vue({
el: "#app",
})
</script>
在生命周期刚挂载到HTML元素的时候,打印出了获取的插槽。是一些vnode对象。
组件高级用法
组件的高级用法一般不用在写业务逻辑,而是用于开发组件中。可以看一下。
组件的name递归调用自身
常用于开发级联的菜单,不知道有几层,所以很方便。不过必须给一个条件来限制最大递归数量。
看一个简单的例子:
<div id="app">
<my-comp :count="1">
</my-comp>
</div>
<script>
Vue.component('my-comp', {
name:'my-comp',
props: {
count: {
type: Number,
default: 1
}
},
template: '<p>这是第{{count}}层递归' +
'<my-comp :count="count+1" v-if="count<3"></my-comp>' +
'</p>'
});
var app = new Vue({
el: "#app",
})
</script>
先给自己指定一个名称,这里名称和注册组件的名称一样,然后在自己的模板里就可以使用了。
每次先给count自增1,然后判断vi-if是否小于3,就可以作出递归的效果。每次相当于又添加了一个子组件,并给子组件传递了count自增之后的结果。
由这个例子还可以看出,在判断v-if和其他属性变动的时候,v-if是先于属性自增判断的。
内联模板
组件的模板是template属性中的字符串,会被解析为HTML然后再被Vue解析。
如果给组件添加inline-template
属性,组件就会把组件标签其中的内容当成模板,而不是当成内容分发的slot插槽内容,这让组件更加灵活。
<div id="app">
<my-comp inline-template>
<div>
<h1>内联模板标题</h1>
<p>内联模板内容</p>
</div>
</my-comp>
</div>
<script>
Vue.component('my-comp', {
template: '<div>template属性中的内容</div>'
});
var app = new Vue({
el: "#app",
})
</script>
这个例子,子组件template属性完全不生效。
如果写成:
<div id="app">
<my-comp inline-template>
<h1>内联模板标题</h1>
<p>内联模板内容</p>
</my-comp>
</div>
就只能渲染出来第一个HTML元素,可见内联模板一样也要遵守template属性中的要求,必须用一个HTML元素包围所有要渲染的内容。
指定要渲染的组件
Vue提供了一个特殊的HTML元素component
,和is属性搭配,用于指定要挂载的组件:
<div id="app">
<component is="comp1"></component>
<hr>
<component is="comp2"></component>
</div>
<script>
Vue.component('comp1', {
template: '<div>comp1的内容</div>'
});
Vue.component('comp2', {
template: '<div>comp2的内容</div>'
});
var app = new Vue({
el: "#app",
})
</script>
异步组件
Vue允许将组件定义成一个工厂函数,在需要的时候调用函数来返回组件进行渲染,还可以把结果缓存起来。
这里先简单看一个小例子:
<div id="app">
<comp></comp>
</div>
<script>
Vue.component('comp', function (resolve, reject) {
window.setTimeout(function () {
resolve({
template: '<div>异步渲染的组件</div>'
})
})
}, 2000);
var app = new Vue({
el: "#app",
})
</script>
更复杂的就是将函数换成异步的promise等方式。这个暂时还不会。待以后再看。
异步更新DOM后执行代码
看一个例子:
<div id="app">
<div id="div" v-if="showDiv">文本</div>
<button @click="getText">获取内容</button>
</div>
<script>
var app = new Vue({
el: "#app",
data: {
showDiv: false,
},
methods: {
getText: function () {
this.showDiv = true;
var text = document.getElementById("div").innerText;
console.log(text);
}
}
})
</script>
这个例子的意图很明显,就是在按下按钮的时候,将文本显示出来,同时将其中的文本输出到控制台。
不过在实际操作的时候,第一次按按钮的时候会报错如下:
[Vue warn]: Error in v-on handler: "TypeError: Cannot read property 'innerText' of null"
(found in <Root>)
TypeError: Cannot read property 'innerText' of null
at Vue.getText (component.html?_ijt=ktcljrpg9soilp14ao257j93ta:26)
at invokeWithErrorHandling (vue.js:1863)
at HTMLButtonElement.invoker (vue.js:2188)
at HTMLButtonElement.original._wrapper (vue.js:7541)
这说明在更新了false之后,其实Vue并没有立刻渲染出这个div,导致获取文本失败。这其实是因为Vue在异步更新队列,会先检测变化,再进行更新。在设置属性为true的时候,在下一个事件循环,Vue检测到属性有变化,才会去创建div,而此时异步的立刻去获取div就会报错。
解决的方法是使用一个$nextTick
来在DOM更新完毕后再执行代码:
<div id="app">
<div id="div" v-if="showDiv">文本</div>
<button @click="getText">获取内容</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").innerText;
console.log(text);
});
}
}
})
</script>
Vue的理念是不要直接操作DOM,而是以数据驱动DOM。如果要使用第三方库,而第三方库是直接操作DOM的,就要使用这个功能。
X-Template
X-Templates是Vue为了组件的模板编写方便而提供的一个便捷的语法糖。
在前边的例子里,编写组件的模板都要通过字符串拼接,行数多了很麻烦。
Vue提供了一个在script
标签上使用的type="text/x-template"
属性:
<div id="app">
<comp></comp>
</div>
<script>
Vue.component("comp",{
template: "#comp"
});
var app = new Vue({
})
</script>
<script type="text/x-template" id="comp">
<p>标签中的模板</p>
</script>
这里给script标签指定了type和id,在组件的template属性里,像用选择器一样选择script标签的id。
然后其中的所有内容就被当成了模板渲染,非常方便。
<div id="app">
<comp></comp>
</div>
<script type="text/x-template" id="comp">
<div>
<p>标签中的模板</p>
<p>{{ count }}</p>
</div>
</script>
<script>
Vue.component('comp', {
template: "#comp",
data: function () {
return {
count: 1
}
},
});
var app = new Vue({
el: "#app"
})
</script>
这里要注意的是,由于是在加载Vue的时候扫描DOM,所以放模板的script标签,要在使用这个模板的Vue代码之前出现。
手动挂载实例
如果一个Vue实例没有el属性或者尚未设置,这个Vue实例就还没有被挂载。
在一些特殊的情况下,可能要手动创建Vue实例然后再指定挂载,Vue提供了extend
和$mount
两个方法手动挂载。
Vue.extend
这是一个基础的Vue实例构造器,接受一个包含vue实例所需要的属性的对象,然后会返回一个“类”,其实就是一个Vue实例的构造器。
然后可以手动使用$mount
来挂载:
<div id="mount">
</div>
<script>
vueobj = Vue.extend({
template: "<p>新创建的Vue实例 {{name}}</p>",
data: function () {
return {
name: "Jenny"
}
}
})
</script>
现在有了一个Vue.extend返回的构造器,还有一个空的div。
可以在浏览器的调试界面内输入:
var target = new vueobj().$mount("#mount")
$mount
也类似一个选择器,选择指定的id元素挂载Vue,输入完这行之后,原来是空的div就会出现内容了,即被挂载上了Vue实例,div的内容会被替换成模板内容。
还可以新创建Vue实例的时候传入el属性:new vueobj({el:"#mount"})
另外还有一个挂载方式和这个不同,不是直接替换元素,而是把一个Vue实例挂到某个元素下边:
var target = new vueobj().$mount();
document.getElementById("mount").appendChild(target.$el)
这样是把Vue实例作为div的子元素挂载。
总结一下,以前一直没有学Vue的组件。看完之后,发现这个东西实际上就想一个Java类,可以在标签上注入所需要的基础数据,然后可以对其各种操作改变状态,同时其对应的DOM元素也会发生变化,还能够通过API来与父组件或者其他组件进行交互。这样就把一个统一渲染的页面,变成了一块一块背后由Vue实例控制的区域,这些区域之间可以互相传递数据,确实很不错。后边根据书上的两个例子写了组件,发现确实很有意思。