一.组件使用的细节点
1.浏览器解析问题
<div id="root">
<table>
<tbody>
<row></row>
<row></row>
<row></row>
</tbody>
</table>
</div>
<script>
Vue.component("row",{
template:"<tr><td>this is a row</td></tr>"
})
var vm = new Vue({
el:"#root"
})
</script>
- 三个tr没有在tbody里面,而tbody要求必须放有tr
- 可以用is属性,让这个tr等价于row
- 这样解析就不会有问题
<div id="root">
<table>
<tbody>
<tr is="row"></tr>
<tr is="row"></tr>
<tr is="row"></tr>
</tbody>
</table>
</div>
<script>
Vue.component("row",{
template:"<tr><td>this is a row</td></tr>"
})
var vm = new Vue({
el:"#root"
})
</script>
- ul-li、select-option也是同样的解决方法
2.组件中的data
- 在子组件中运用data,页面报错
<div id="root">
<table>
<tbody>
<tr is="row"></tr>
<tr is="row"></tr>
<tr is="row"></tr>
</tbody>
</table>
</div>
<script>
Vue.component("row",{
data:{
content:"this is a row"
},
template:"<tr><td>{{content}}</td></tr>"
})
var vm = new Vue({
el:"#root"
})
</script>
- data应该是function 而不是对象
- 在最外层的vue实例中定义data用对象没问题
- 但是在非根组件中,比如子组件定义时只能用function,而不能用对象格式
<div id="root">
<table>
<tbody>
<tr is="row"></tr>
<tr is="row"></tr>
<tr is="row"></tr>
</tbody>
</table>
</div>
<script>
Vue.component("row",{
data:function(){
return {
content:"this is a row"
}
},
template:"<tr><td>{{content}}</td></tr>"
})
var vm = new Vue({
el:"#root"
})
</script>
- 子组件定义data,data只能是函数,而不能是对象
- 一个子组件不想根组件只调用一次,它可能在不同的地方,比如row子组件调用三次
- 每个子组件的数据不希望和其他的子组件产生冲突(每个子组件应该有自己的数据,比如上面的row是三套数据,而不是共享一个数据)
- 通过一个函数返回一个对象的目的,让子组件拥有独立的数据存储,不会出现多个子组件相互影响的情况
3.vue中的ref引用
- vue不建议我们在代码中操作dom
- 但是复杂的动画效果,光靠vue数据绑定是不行的,所以有些必要情况下必须要操作dom
- vue通过ref引用形式操作dom
<div id="root">
<div ref="hello" @click="handleClick">hello world</div>
</div>
<script>
Vue.component("row",{
data:function(){
return {
content:"this is a row"
}
},
template:"<tr><td>{{content}}</td></tr>"
})
var vm = new Vue({
el:"#root",
methods:{
handleClick:function () {
alert(this.$refs.hello.innerHTML)
}
}
})
</script>
- vue所有的引用下的名为hello的引用,对应的就是这个div的dom,指向的就是这个div的dom节点
- 如果一个组件上加了ref,this.$refs.xxx就是对应这个组件的引用
- 实现一个点击第一、二行都自增1,第三行显示第一行+第二行的和,这里就要用到ref引用
<div id="root">
<counter ref="one" @change="handleChange"></counter>
<counter ref="two" @change="handleChange"></counter>
<div>{{total}}</div>
</div>
<script>
Vue.component("counter",{
template:"<div @click='handleClick'>{{number}}</div>",
data:function(){
return{
number:0
}
},
methods:{
handleClick:function () {
this.number++;
this.$emit("change")
}
}
})
var vm = new Vue({
el:"#root",
data:{
total:0
},
methods:{
handleChange:function () {
this.total = this.$refs.one.number + this.$refs.two.number
}
}
})
</script>
二.父子组件的间的数据传递
1.父组件向子组件数据传递
- 此时count中的0和1都只是字符串
<div id="root">
<counter count="0"></counter>
<counter count="1"></counter>
</div>
<script>
var counter = {
template: "<div>0</div>"
}
var vm = new Vue({
el:"#root",
components:{
counter:counter
}
})
</script>
- 加了冒号就是js表达式,也就是数字类型
<div id="root">
<counter :count="0"></counter>
<counter :count="1"></counter>
</div>
<script>
var counter = {
template: "<div>0</div>"
}
var vm = new Vue({
el:"#root",
components:{
counter:counter
}
})
</script>
- 子组件要接收这个数据才能使用,所以用props属性来接收
<div id="root">
<counter :count="0"></counter>
<counter :count="1"></counter>
</div>
<script>
var counter = {
props:['count'],
template: "<div>{{count}}</div>"
}
var vm = new Vue({
el:"#root",
components:{
counter:counter
}
})
</script>
- 子组件累加的功能
<div id="root">
<counter :count="0"></counter>
<counter :count="1"></counter>
</div>
<script>
var counter = {
props:['count'],
template: "<div @click='handleAdd'>{{count}}</div>",
methods:{
handleAdd:function () {
this.count++
}
}
}
var vm = new Vue({
el:"#root",
components:{
counter:counter
}
})
</script>
虽然可以实现,但是还是报了警告
- 不能直接修改父组件传递过来的数据,vue有个单向数据流的概念,一旦接收的count不是基础类型数据,如果是对象形式,引用类型时,子组件改变了传递过来的内容,有可能接收的引用的数据还在被其他子组件所使用,这样子组件不仅影响了自己,还影响了其他子组件。所以不能改变父组件传递过来的数据
- 我们可以拷贝一份来实现这个累加功能
<div id="root">
<counter :count="0"></counter>
<counter :count="1"></counter>
</div>
<script>
var counter = {
props:['count'],
data:function(){
return{
number:this.count
}
},
template: "<div @click='handleAdd'>{{number}}</div>",
methods:{
handleAdd:function () {
this.number++
}
}
}
var vm = new Vue({
el:"#root",
components:{
counter:counter
}
})
</script>
2.子组件向父组件数据传递
- 简单说就是让子组件向外触发事件
- 父组件监听这个事件
<div id="root">
<counter :count="2" @change="handleIncrease"></counter>
<counter :count="3" @change="handleIncrease"></counter>
<div>{{total}}</div>
</div>
<script>
var counter = {
props:['count'],
data:function(){
return{
number:this.count
}
},
template: "<div @click='handleAdd'>{{number}}</div>",
methods:{
handleAdd:function () {
this.number = this.number + 2
this.$emit("change",2)
}
}
}
var vm = new Vue({
el:"#root",
components:{
counter:counter
},
data:{
total:5
},
methods:{
handleIncrease:function (msg) {
this.total = this.total + msg
}
}
})
</script>
三.组件参数校验与非props特性
1.组件参数校验
- 平常情况下都这么写的
<div id="root">
<counter count="123"></counter>
<counter count="321"></counter>
</div>
<script>
Vue.component("counter",{
props:["count"],
template:"<div>{{count}}</div>"
})
var vm = new Vue({
el:"#root"
})
</script>
- 如果你要加参数校验,就要把props从数组改成对象
- 要求count为字符串(加了冒号123就变成数字了,所以报错)
<div id="root">
<counter count="hello"></counter>
<counter :count="123"></counter>
</div>
<script>
Vue.component("counter",{
props:{
count:String
},
template:"<div>{{count}}</div>"
})
var vm = new Vue({
el:"#root"
})
</script>
- 可以设置成多种类型
<div id="root">
<counter count="hello"></counter>
<counter :count="123"></counter>
</div>
<script>
Vue.component("counter",{
props:{
count:[String,Number]
},
template:"<div>{{count}}</div>"
})
var vm = new Vue({
el:"#root"
})
</script>
- 此时传对象就会报错
<div id="root">
<counter :count="{a:1}"></counter>
</div>
- 数据后边也可以跟着对象(类型、是否必要、默认值)
<div id="root">
<counter></counter>
</div>
<script>
Vue.component("counter",{
props:{
count:{
type:String,
required:false,
default:'哈哈'
}
},
template:"<div>{{count}}</div>"
})
var vm = new Vue({
el:"#root"
})
</script>
- 以及要求长度之类的validator对象(校验器)
<div id="root">
<counter count="hello world"></counter>
</div>
<script>
Vue.component("counter",{
props:{
count:{
type:String,
validator:function (value) {
return (value.length > 5)
}
}
},
template:"<div>{{count}}</div>"
})
var vm = new Vue({
el:"#root"
})
</script>
2.props特性
- 父组件使用子组件时,通过属性子组件传值时,子组件申明了对父组件传递过来的数据的接收
- 比如父组件传递content 给子组件,子组件恰好又申明了content
- 属性传递不会在dom节点上显示的,子组件也能把传递数据通过插值表达式显示出来
<div id="root">
<child content="hello world"></child>
</div>
<script>
Vue.component("child",{
props:{
content:{
type:String
}
},
template:"<div>{{content}}</div>"
})
var vm = new Vue({
el:"#root"
})
</script>
3.非props特性
- 父组件传递数据,子组件并没有申明接收父组件传递的内容
<div id="root">
<child content="hello world"></child>
</div>
<script>
Vue.component("child",{
template:"<div>hello</div>"
})
var vm = new Vue({
el:"#root"
})
</script>
- 虽然会报错,但是网页会显示dom属性
- 实际情况下非props特性很少用到
四.给组件绑定原生事件
- 定义一个全局的子组件,按逻辑实例中methods写这个方法,但是会发现没有用
<div id="root">
<child @click="handleClick"></child>
</div>
<script>
Vue.component("child",{
template:"<div>Child</div>",
methods:{
handleClick:function () {
alert('1')
}
}
})
var vm = new Vue({
el:"#root"
})
</script>
- 当给组件绑定事件的时候,这个事件绑定的是自定义的事件
- 真正鼠标点击触发的事件并不是我绑定的这个click事件
- 在组件中给元素绑定事件,这个才是原生事件,而Child绑定的click是自定义事件
- 父组件这里自定义事件监听用this.$emit
- 点击时,子组件会监听到div元素被点击了,然后执行这个自定义的事件,向外触发一个自定义click事件,父组件用子组件监听了这个事件,所以对应代码会执行 ,但是这样写代码很麻烦
<div id="root">
<child @btn="handleClick"></child>
</div>
<script>
Vue.component("child",{
template:"<div @click='handleChildClick'>Child</div>",
methods:{
handleChildClick:function () {
alert('handleChildClick')
this.$emit("btn")
}
}
})
var vm = new Vue({
el:"#root",
methods:{
handleClick:function(){
alert('handleClick')
}
}
})
</script>
- 如果非要child监听原生的事件,可以加上.native修饰符就可以
<div id="root">
<child @click.native="handleChildClick"></child>
</div>
<script>
Vue.component("child",{
template:"<div>Child</div>",
})
var vm = new Vue({
el:"#root",
methods:{
handleChildClick:function () {
alert('handleChildClick')
}
}
})
</script>
五.非父子组件间的传值
1.非父子组件间的传值介绍
- 假如我们拆分成3个组件
- 第一层和第二层传递数据就是父子组件传值
- 父组件通过props向子组件传值
- 子组件通过向外触发事件向父组件传值
- 那么万一是第一层和第三层传递数据呢?
- 不能通过属性直接传递数据
- 把第一层数据传给第二层然后在传给第三层…反过来也一样,但是这样就复杂了
- 万一是左边第3层像右边第3层传值呢?难道还一层一层传递?
- Vue是轻量级视图框架,不是把事情变复杂的
- 如果遇到复杂的数据传递,光靠vue框架是解决不了问题的,要引入一些设计模式之类的
- 一般有两种解决方式(解决复杂的非父子组件传值)
- 借助vue官方的数据层框架vuex(使用起来有难度)—后边结合项目来说
- 使用bus格式来解决(vue称为总线机制/发布订阅模式/观察者模式)
2.总线机制
- 我们通过组件间传值例子来讲解
- 在vue实例挂载一个bus属性,bus属性又指向vue的实例
- this.bus指的就是实例上挂载的bus,每个实例上都会有bus这个属性,而bus又是一个vue实例,所以它下面就会有$emit方法
<div id="root">
<child content="hello"></child>
<child content="world"></child>
</div>
<script>
Vue.prototype.bus = new Vue()
Vue.component("child",{
props:{
content:{
type:String
}
}
template:"<div @click='handleClick'>{{content}}</div>",
methods:{
handleClick:function () {
this.bus.$emit('change',this.content)
}
}
})
var vm = new Vue({
el:"#root"
})
</script>
- 接下来就让其他组件监听这个事件,可以借助生命周期钩子mounted(组件被挂载后执行的函数),$on监听bus触发的事件
<div id="root">
<child content="hello"></child>
<child content="world"></child>
</div>
<script>
Vue.prototype.bus = new Vue()
Vue.component("child",{
props:{
content:{
type:String
}
}
template:"<div @click='handleClick'>{{content}}</div>",
methods:{
handleClick:function () {
this.bus.$emit('change',this.content)
}
},
mounted:function(){
var this_ = this
this.bus.$on('change',function (msg) {
this_.content = msg //注意作用域
})
}
})
var vm = new Vue({
el:"#root"
})
</script>
- 这样两个组件传值通过总线模式就搞定了,但是没有问题了吗?
- 虽然实现,但是看浏览器还是会报警告(因为content是父组件传来的数据,在vue中要时刻有单项数据流的概念)
- 可以用子组件拷贝父组件传来的数据的方法
<div id="root">
<child content="hello"></child>
<child content="world"></child>
</div>
<script>
Vue.prototype.bus = new Vue()
Vue.component("child",{
props:{
content:{
type:String
}
},
data:function(){
return {
selfContent : this.content
}
},
template:"<div @click='handleClick'>{{selfContent}}</div>",
methods:{
handleClick:function () {
this.bus.$emit('change',this.selfContent)
}
},
mounted:function(){
var this_ = this
this.bus.$on('change',function (msg) {
this_.selfContent = msg //注意作用域
})
}
})
var vm = new Vue({
el:"#root"
})
</script>
六.在vue中使用插槽
1.插槽的使用场景
- 子组件除了展示p标签外,还要展示一段内容,而这段内容不是子组件决定的,是父组件传递过来的
- 以前的想法怎么传?可以通过属性形式向子组件传值
- 但是你会发现做了转义
<div id="root">
<child content='<p>world</p>'></child>
</div>
<script>
Vue.component('child',{
props:["content"],
template:'<div><p>hello</p> {{content}}</div>'
})
var vm = new Vue({
el:"#root"
})
</script>
- 所以只能用模板占位符
<div id="root">
<child content='<p>world</p>'></child>
</div>
<script>
Vue.component('child',{
props:["content"],
template:'<div><p>hello</p> <div v-html="this.content"></div></div>'
})
var vm = new Vue({
el:"#root"
})
</script>
- 这种形式传递少可以,但传递一旦多了,显示没问题,但是代码很难阅读,插槽就是解决这个问题出来的
2.插槽的使用
- 显示的就是父组件向子组件插入进来的这个p标签
<div id="root">
<child>
<h1>world</h1>
</child>
</div>
<script>
Vue.component('child',{
template:'<div> <p>hello</p> <slot></slot> </div>'
})
var vm = new Vue({
el:"#root"
})
</script>
- 也方便向子组件传递dom元素,同时子组件使用插槽内容也很简单
- 也可以在slot标签内写默认内容
3.插槽的命名
- 如果要实现header、content、footer用如下代码是不行的
- 子组件全部都是插槽的内容,所以就会出现如下下图的样子,相当于调用了两次
- 此时要给插槽命名
<div id="root">
<body-content>
<div class="header">header</div>
<div class="footer">footer</div>
</body-content>
</div>
<script>
Vue.component('body-content',{
template:'<div> <slot></slot> <div class="content">content</div> <slot></slot></div>'
})
var vm = new Vue({
el:"#root"
})
</script>
<div id="root">
<body-content>
<div class="header" slot="header">header</div>
<div class="footer" slot="footer">footer</div>
</body-content>
</div>
<script>
Vue.component('body-content',{
template:'<div> <slot name="header"></slot> <div class="content">content</div> <slot name="footer"></slot></div>'
})
var vm = new Vue({
el:"#root"
})
</script>
- 当然也可以设置默认值(当没有同名的插槽)
<div id="root">
<body-content>
<div class="footer" slot="footer">footer</div>
</body-content>
</div>
<script>
Vue.component('body-content',{
template:'<div> <slot name="header"><h1>default slot</h1></slot> <div class="content">content</div> <slot name="footer"></slot></div>'
})
var vm = new Vue({
el:"#root"
})
</script>
七.作用域插槽
1.作用域插槽的使用
- 循环遍历列表
<div id="root">
<child></child>
</div>
<script>
Vue.component('child',{
data:function(){
return {
list:[1,2,3,4]
}
},
template:'<div><ul> <li v-for="item of list">{{item}}</li> </ul></div>'
})
var vm = new Vue({
el:"#root"
})
</script>
- 这个child组件会在很多地方调用,列表样式应该不是由child控制的,而是由外部决定的
<div id="root">
<child>
<template slot-scope="props">
<h1> {{props.item}} -- hello </h1>
</template>
</child>
</div>
<script>
Vue.component('child',{
data:function(){
return {
list:[1,2,3,4]
}
},
template:'<div><ul> <slot v-for="item of list" :item=item></slot> </ul></div>'
})
var vm = new Vue({
el:"#root"
})
</script>
- 当子组件用slot时,会往slot传递一个item数据,item数据放在slot-scope的属性之中
- 这就不是由子组件决定的了,而是父组件调用子组件的时候给子组件去传递对应的模板,这就是作用域槽的使用
- 父组件调用子组件时,给子组件传了一个插槽,这个插槽是作用域插槽,作用域插槽必须是template的,申明从子组件接收的数据slot-scope放在那里,告诉子组件模板信息
- 当子组件做循环或者某一个部分dom结构由外部传递进来的时候用作用域插槽,子组件可以像父组件插槽传递数据
,父组件插槽向接收这个数据,必须外层申明一个template的,接收传递数据的放在那里,传递一个item过来 ,接收到这个item
八.动态组件与 v-once 指令
1.v-once指令
- 实现一个动态互相展示的效果
- 不过这种方式都是创建一次 销毁一次(重新),很消耗性能
<div id="root">
<child-one v-if="type ==='child-one' "></child-one>
<child-two v-if="type ==='child-two' "></child-two>
<button @click="handleClick">交换</button>
</div>
<script>
Vue.component('child-one',{
template:'<div>child-one</div>'
})
Vue.component('child-two',{
template:'<div>child-two</div>'
})
var vm = new Vue({
el:"#root",
data:{
type:'child-one'
},
methods:{
handleClick:function () {
this.type = (this.type === 'child-one'? 'child-two':'child-one')
}
}
})
</script>
- 可以用v-once指令优化
- 第一次展示后都会放到内存里面,切换不会重新创建组件,直接去内存中拿的,性能更高
<div id="root">
<child-one v-if="type ==='child-one' "></child-one>
<child-two v-if="type ==='child-two' "></child-two>
<button @click="handleClick">交换</button>
</div>
<script>
Vue.component('child-one',{
template:'<div v-once>child-one</div>'
})
Vue.component('child-two',{
template:'<div v-once>child-two</div>'
})
var vm = new Vue({
el:"#root",
data:{
type:'child-one'
},
methods:{
handleClick:function () {
this.type = (this.type === 'child-one'? 'child-two':'child-one')
}
}
})
</script>
2.动态组件
- 也可以用动态组件的形式来展示这个效果
- component是vue自带的,会根据type属性自带的变化加载不同的组件
<div id="root">
<component :is="type"></component>
<button @click="handleClick">交换</button>
</div>
<script>
Vue.component('child-one',{
template:'<div>child-one</div>'
})
Vue.component('child-two',{
template:'<div>child-two</div>'
})
var vm = new Vue({
el:"#root",
data:{
type:'child-one'
},
methods:{
handleClick:function () {
this.type = (this.type === 'child-one'? 'child-two':'child-one')
}
}
})
</script>