文章目录
项目很庞大的时候,希望开发的时候能分模块开发,就像现在提倡的组件化开发,一个自定义标签 - vue就会把他看成一个组件,vue可以给这些标签赋予一定的意义
根据功能—组件分两类:
- 页面级组件
- 基础组件 - 将可复用的部分抽离出来
根据用法—组件分为:
- 全局组件 — 声明一次,在任何地方使用 (一般写插件的时候,用全局组件多一些;)
- 局部组件 — 必须告诉这个组件属于谁
1. 组件命名
- 组件名不要带有大写(最多可以首字母大写),多个单词用中划线(蛇形/烤串命名发)
<My-div></My-div>
/<my-div></my-div>
- 组件名 和定义名字要相同
- html中采用短横线隔开命名法,js中转小驼峰也是可以的
2 . 全局组件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="shortcut icon" href="#" type="image/x-icon">
</head>
<body>
<div id="app">
<my-div></my-div>
</div>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
let vm = new Vue({
el:'#app',
data:{
a:'hello',
arr:[1,2,3],
},
})
//定义全局组件
Vue.component('my-div',{ //'my-div'是组件名, 一个对象可以看成一个组件
template:'<div>{{msg}}</div>', //在自己的模板中使用自己的数据; 用模板里的内容,替换掉<my-div></my-div>
data(){ //组件中的数据必须是函数类型的,return返回一个实例 对象 作为组件的数据
return {
msg:'你好'
}
}
})
</script>
</body>
</html>
3. 局部组件
- 局部组件使用三部曲:1.创建组件 2.注册组件 3.使用组件
- 组件是相互独立的,不能跨作用域,vm实例也是一个组件–所以vm上的数据,组件是取不到的
子组件不能直接使用父组件(vm)的数据;–组件之间的数据交互- 实例上的声明周期函数,组件也有自己的;
- 如果组件公用了数据,会导致同时更新(这样组件就没有 独立性 了)
- 组件可以套组件,组件理论上可以无限嵌套;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="shortcut icon" href="#" type="image/x-icon">
</head>
<body>
<div id="app">
<comp1></comp1>
<comp2></comp2>
<!-- 3.使用组件 -->
</div>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
//局部组件使用三部曲:1.创建组件 2.注册组件 3.使用组件
let comp1 = { template:'<div>你好</div>'}; //1.创建组件,组件就是一个对象,里面必须有template属性
let comp2 = {
template:'<div>hello</div>',
data(){ //为什么写个函数,返回一个对象就独立了?因为掉用一个函数,产生一个新作用域(新对象),调用两个函数返回的对象,(空间)永远都不会一样,不会出现共用数据的问题,所以需要是个函数;
return {};
}
};
let vm = new Vue({
el:'#app',
components:{
comp1, //2.注册组件,comp:comp, es6中名字一样可以简写为 comp
comp2,
}
})
</script>
</body>
</html>
为什么组件里,写个函数返回一个对象就独立了?
因为调用一个函数,产生一个新作用域(新对象),调用两个函数返回的对象,(空间)永远都不会一样,不会出现共用数据的问题,所以需要是个函数;
4. 嵌套组件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="shortcut icon" href="#" type="image/x-icon">
</head>
<body>
<div id="app">
<parent></parent>
<!-- 33.使用组件 -->
</div>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
let grandson = { template:'<div>grandson</div>'};
let son = {
template:'<div>son<grandson></grandson></div>',
components:{ grandson,}
};
let parent = {
template:`
<div>parent<son></son></div>`,
components:{ son,}
};
let vm = new Vue({
el:'#app',
components:{
parent,
}
})
</script>
</body>
</html>
5.发布订阅
发布订阅:一对多的依赖关系,让多个订阅者对象同时监听一个主题的变化,模拟发布订阅:
//发布订阅:一对多的依赖关系,让多个订阅者对象同时监听一个主题的变化
// vm.$on绑定事件 vm.$once绑定一次 vm.$off解绑事件 vm.$emit 触发事件
function Girl(){ //构造函数
this._events = {}; //这个变量,on 和 emit 方法都能调用,但是又是私有的,所以卸载构造函数里
}
//让每个实例都有 on /emit 方法,就把方法写到构造函数的原型上
Girl.prototype.on = function(eventName,callback){
// {shilian:[fn1,fn2,fn3]} --组成这个形式-一对多的关系
if(this._events[eventName]){ //判断是不是第一次给这个事件订阅方法
this._events[eventName].push(callback);
}else{
this._events[eventName] = [callback];
}
}
Girl.prototype.emit = function(eventName,...args){
// Array.from(arguments).slice(1);
// [].slice.call(arguments,1);
//触发事件eventName,执行该事件订阅的方法
if(this._events[eventName]){ //判断是否订阅过方法
this._events[eventName].forEach(ele =>{
ele(...args);
// ele.apply(this,args);
})
}
}
let girl = new Girl(); //实例化一个girl
// 定义方法
let fn1 = (arg1,arg2)=>{console.log(`${arg1}哭`);}
let fn2 = (arg1,arg2)=>{console.log(`在${arg2}十六了,哭`);}
let fn3 = (arg1,arg2)=>{console.log('买');}
// 绑定/订阅 方法到实例上,等实例事件触发的时候调用订阅的方法
girl.on('shilian', fn1); // {shilian:[fn1]}
girl.on('shilian', fn2); // {shilian:[fn1,fn2]}
girl.on('shilian', fn3); //把一个事件订阅的方法放在一起:{shilian:[fn1,fn2,fn3]}
//触发/发布 事件
girl.emit('shilian','参数1','参数2'); //触发失恋,这个事件订阅的方法都会被调用
6. 组件间通信
父 传 子(属性传递)
父组件在使用子组件时,通过属性传递数据
<son :a='111'></son>
,子组件在定义时,通过props来接收数据props:['a']
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="shortcut icon" href="#" type="image/x-icon">
</head>
<body>
<div id="app">
<parent></parent>
<!-- 33.使用组件 -->
</div>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
let grandson = {
template:`<div>grandson{{m}}</div>`,
props:{ //对象形式可以进行验证
m:{
type:[String,Function,Number], //限制传的数据类型,会报错但不会阻止代码执行
// default:0,
required:true, //不能和default共用
validator(val){
return val > 300;
}
}
}
};
let son = {
template:`<div>son{{a}}<grandson :m='400'></grandson></div>`,
components:{ grandson,},
props:['a'] //子组件中接收父组件传的数据
};
let parent = {
template:`
<div>parent<son :a='111'></son></div>`,
components:{ son,}
};
let vm = new Vue({
el:'#app',
components:{
parent,
}
})
</script>
</body>
</html>
子 传 父(触发事件传递:传子的数据,触发父的方法)
使用组件时,所有属性名和事件名都是子组件的,属性值 和 事件方法都是父组件的
vm.$on
绑定事件vm.$once
绑定一次vm.$off
解绑事件vm.$emit
触发事件- 给父亲绑定一些事件,儿子触发事件时将数据传递过去
- 单向数据流:父亲数据刷新,儿子数据就刷新
- 属性值(方法)是父亲的,属性是儿子的(自定义属性/事件名)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="shortcut icon" href="#" type="image/x-icon">
</head>
<body>
<div id="app">
<!-- 子传父3: 通过子组件自己的 自定义事件名,找到绑定的父组件的 方法 -->
<son @sonclick='parentFn' :parentnum='num'></son> <!-- 属性值(方法)是父亲的,属性是儿子的(自定义属性/事件名)-->
</div>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
let son = {
// 子传父1: 点击子组件里某元素,触发自己的事件add
template:`<div @click = 'add'>点击后加{{a}}变为:{{parentnum}}</div>`, //使用父组件里的数据
props:['parentnum'], //子组件中接收父组件传的数据
data(){
return {
a:10,
}
},
methods:{
add(){
// 子传父2: 在事件add中,触发自己的自定义事件名,并传参数过去
this.$emit('sonclick',this.a);
}
}
};
let vm = new Vue({
el:'#app',
data:{
num:9,
},
components:{
son,
},
methods:{
// 子传父4: 在方法中操作子组件传来的数据
parentFn(val){
this.num += val;
}
}
})
</script>
</body>
</html>
案例:模态框
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="shortcut icon" href="#" type="image/x-icon">
<style>
.mask{
width:100%;
height:100%;
position: fixed;
top:0;
left:0;
background: #000;
opacity:0.5;
}
.dialog{
width:400px;
height:300px;
background:#fff;
position:fixed;
left: 50%;
top:50%;
transform:translate3d(-50%,-50%,0);
}
</style>
</head>
<body>
<div id="app">
<button @click='flag = !flag'>弹窗</button>
<model :turn = 'flag' @myclose='fn'></model>
</div>
<template id='model'>
<div class="mask" v-if='turn'>
<div class="dialog">
<button @click='close'>关闭</button>
</div>
</div>
</template>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
let model = {
template:'#model',
data(){
return {}
},
props:['turn'],
methods:{
close(){
this.$emit('myclose')
}
}
};
let vm = new Vue({
el:'#app',
components:{
model,
},
data:{
flag:false,
},
methods:{
fn(){
this.flag = false;
}
}
})
</script>
</body>
</html>
父组件操作子组件的方法–ref
<comp ref='load'></comp>
使用组件的时候给一个ref属性,然后再父组件里
this.$refs.load
是comp组件实例,
this.$refs.load.close()
是获取子组件里的方法并执行
this.$refs.load.$el
是获取子组件comp的根标签div元素
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="shortcut icon" href="#" type="image/x-icon">
</head>
<body>
<div id="app">
<comp ref='load'></comp>
</div>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
let comp = {
template:`<div v-show='flag'>加载中...</div>`,
data(){
return {
flag:true,
}
},
methods:{
close(){
this.flag = false;
}
}
};
let vm = new Vue({
el:'#app',
mounted(){ //mounted里dom元素加载完成了,关闭掉loading
//ref属性加在dom上,this.$refs获取的是dom,如果ref放在组件上,获取的是组件的实例,并不是组件的dom元素
// this.$refs.load.close();
this.$refs.load.$el.style.background = 'red';
},
components:{
comp,
},
})
</script>
</body>
</html>
非父子组件通信–eventBus事件车
下面是最初的想法,但是由于发布和订阅不在同一个实例上,无法互相找到和被触发,所以引入一个实例(创建一个第三方实例)作为中间体,发布和订阅都写到这上面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="shortcut icon" href="#" type="image/x-icon">
</head>
<body>
<div id="app">
<brother1></brother1>
<brother2></brother2>
</div>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
// 组件都有自己独立的作用域,非父子关系的组件的通信,如果是简单业务就用eventbus(),复杂的就用vueX
let brother1 = {
template:`<div>{{color}} <button @cha>让2变成红色</button></div>` ,
created(){
/* 发布订阅:点击2 要改变1的颜色,
1是被改变的--在1上订阅,所以在1careated里一加载完就放上事件监听
点击2--在2上触发,所以在2里写点击 时 触发 1上监听的事件
最初的想法是这样的,但是由于发布和订阅不在同一个实例上,无法互相找到和触发,
所以引入一个实例(创建一个第三方实例)作为中间体,发布和订阅都写到这上面*/
// 1. 先在自己组件里一加载到时候,监听一个事件,这个事件被触发时执行后面的函数
this.$on('changeGreen',(val)=>{ //这里必须是箭头函数,回调用function里面的this都是window,用箭头函数this都指向当前实例
this.color = val;
})
},
data(){
return {color:'1原始红色',old:'红色'}
}
}
let brother2 = {
template:`<div>{{color}} <button @click='change'>让1变成绿色</button></div>` ,
data(){
return { color:'2原始绿色',old:'绿色'}
},
methods:{
// 2.点击事件时调用change方法,然后在方法里触发订阅的changeGreen
change(){
this.$emit('changeGreen',this.old);
}
}
}
let vm = new Vue({
el:'#app',
data:{
},
components:{
brother1,
brother2
},
methods:{
}
})
</script>
</body>
</html>
最终写法:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="shortcut icon" href="#" type="image/x-icon">
</head>
<body>
<div id="app">
<brother1></brother1>
<brother2></brother2>
</div>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
// 组件都有自己独立的作用域,非父子关系的组件的通信,如果是简单业务就用eventbus(),复杂的就用vueX
let eventBus = new Vue; //3. 定义一个事件车,不需要传参
let brother1 = {
template:`<div>{{color}} <button @cha>让2变成红色</button></div>` ,
created(){
/* 发布订阅:点击2 要改变1的颜色,
1是被改变的--在1上订阅,所以在1careated里一加载完就放上事件监听
点击2--在2上触发,所以在2里写点击 时 触发 1上监听的事件
最初的想法是这样的,但是由于发布和订阅不在同一个实例上,无法互相找到和触发,
所以引入一个实例(创建一个第三方实例)作为中间体,发布和订阅都写到这上面*/
// 1. 先在自己组件里一加载到时候,监听一个事件,这个事件被触发时执行后面的函数
eventBus.$on('changeGreen',(val)=>{ //这里必须是箭头函数,回调用function里面的this都是window,用箭头函数this都指向当前实例
this.color = val; //4.把监听事件放到eventBus
})
},
data(){
return {color:'1原始红色',old:'红色'}
}
}
let brother2 = {
template:`<div>{{color}} <button @click='change'>让1变成绿色</button></div>` ,
data(){
return { color:'2原始绿色',old:'绿色'}
},
methods:{
// 2.点击事件时调用change方法,然后在方法里触发订阅的changeGreen
change(){
eventBus.$emit('changeGreen',this.old); //5.触发的时候触发eventBus上的事件
}
}
}
let vm = new Vue({
el:'#app',
data:{
},
components:{
brother1,
brother2
},
methods:{
}
})
</script>
</body>
</html>
7. 插槽slot
slot作用:定制模板,用共同的模板插入不同的内容
- 没有solt属性的内容 默认放到 name为default的slot标签中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="shortcut icon" href="#" type="image/x-icon">
</head>
<body>
<div id="app">
<comp>lxz</comp>
<!--1. 如果没有 slot属性,组件标签中的内容 会被插到组件里的 <slot></slot>中,也就是<slot name='default'></slot>-->
<hr>
<comp>
<p slot='msg1'>你好!</p>
<span slot='msg2'>请登录!</span>
<!--2. 如果有 slot属性,标签中的内容 会被插到组件里的slot标签中对应的name属性的标签里 <slot name='msg2'></slot>-->
</comp>
</div>
<template id='comp'>
<div>
<slot></slot> <!--1. <slot></slot> 等于 <slot name='default'></slot> -->
<slot name='msg1'></slot>
<!-- 2.显示传入的 -->
<slot name='msg3'>传就显示传的内容,没有传入内容,就显示这里的默认内容</slot>
<!-- 3.传就显示传的,没传就显示默认的 -->
<slot name='msg2'></slot>
<slot name='msg4'></slot>
<!-- 4.传就显示,没传就不显示 -->
</div>
</template>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
let comp = {
template:'#comp',
};
let vm = new Vue({
el:'#app',
components:{
comp,
},
})
</script>
</body>
</html>
8. 组件切换 与 缓存
1.
<component :is='val'></component>
这是vue自带的标签,不用声明可以直接用类似的还有template、slot 、transition、transiton-group
2. 组件切换的时候,会进行销毁旧的然后挂载新的,但是我们反复切换的时候,希望缓存组件,可以把组件放到keep-alive
标签里
<keep-alive>
<component :is='val'></component>
</keep-alive>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="shortcut icon" href="#" type="image/x-icon">
</head>
<body>
<div id="app">
<input type="radio" v-model='val' value='home'>home
<input type="radio" v-model='val' value='list'>list
<keep-alive>
<component :is='val'></component>
</keep-alive>
<!-- 这是vue自带的标签,不用声明可以直接用类似的还有template、slot 、transition、transiton-group -->
<!-- 组件切换的时候,会进行销毁旧的然后挂载新的,但是我们反复切换的时候,希望缓存组件,可以把组件放到keep-alive标签里 -->
<!-- <keep-alive></keep-alive> 一般用作缓存:为后面的路由做准备
以前页面的切换都是点击一下重新渲染一下,但是路由的功能是可以缓存,把组件缓存起来
如果缓存了就不会再走created 、mounted等钩子函数了
-->
<!-- 子组件和父组件同时拥有mounted方法,会先走谁的?
mounted-挂载完,父组件挂载前需要先确定子组件挂载完了
需要等待子组件挂载完成后再触发父组件的挂载
-->
</div>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
//keep-alive 保持连接 ,组件有个方法:销毁
let home ={
template:'<div>home</div>'
}
let list ={
template:'<div>list</div>'
}
let vm = new Vue({
el:'#app',
data:{
val:'home'
},
components:{
home,
list
},
})
</script>
</body>
</html>
9. 强调 this.$nextTick(callback)
dom渲染是异步的,当我们改变数据时,有可能dom重新渲染还没渲染完,强烈建议把dom操作放在this.$nextTick(callback)
的回调里,等dom渲染完再执行回调函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="shortcut icon" href="#" type="image/x-icon">
</head>
<body>
<div id="app">
<son ref='son'></son>
</div>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
let vm = new Vue({
el:'#app',
mounted(){
console.log(this.$refs.son.$el.innerHTML); //<ul><li>1</li><li>2</li><li>3</li></ul>
//2.然后走父组件的mounted,此时子组件的dom渲染还没渲染完,所以获取到的是之前没改数据前的
this.$nextTick( ()=>{
console.log(this.$refs.son.$el.innerHTML) ; //<ul><li>1</li><li>2</li><li>3</li></ul>
//3.所以强烈建议把dom操作放在this.$nextTick(callback)的回调里,等dom渲染完再执行回调函数
}
);
},
components:{
son:{
template:`<div><ul><li v-for='item in arr'>{{item}}</li></ul></div>`,
data(){
return {
arr:[1,2,3]
}
},
mounted(){ //1.此时dom已经渲染完了,页面数据是123,先执行子组件的mounted,在mounted里数据改了,然后重新渲染dom,但dom渲染是异步的,
this.arr = [4,5,6];
}
}
},
})
</script>
</body>
</html>