【Vue】9 - 组件(全局、局部)、通信(props、$meit、$refs)、插槽slot、component、$nextTick等

项目很庞大的时候,希望开发的时候能分模块开发,就像现在提倡的组件化开发,一个自定义标签 - vue就会把他看成一个组件,vue可以给这些标签赋予一定的意义
根据功能—组件分两类:

  • 页面级组件
  • 基础组件 - 将可复用的部分抽离出来

根据用法—组件分为:

  • 全局组件 — 声明一次,在任何地方使用 (一般写插件的时候,用全局组件多一些;)
  • 局部组件 — 必须告诉这个组件属于谁

1. 组件命名

  1. 组件名不要带有大写(最多可以首字母大写),多个单词用中划线(蛇形/烤串命名发)<My-div></My-div> / <my-div></my-div>
  2. 组件名 和定义名字要相同
  3. 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. 局部组件使用三部曲:1.创建组件 2.注册组件 3.使用组件
  2. 组件是相互独立的,不能跨作用域,vm实例也是一个组件–所以vm上的数据,组件是取不到的
    子组件不能直接使用父组件(vm)的数据;–组件之间的数据交互
  3. 实例上的声明周期函数,组件也有自己的;
  4. 如果组件公用了数据,会导致同时更新(这样组件就没有 独立性 了)
  5. 组件可以套组件,组件理论上可以无限嵌套;
<!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>

子 传 父(触发事件传递:传子的数据,触发父的方法)

使用组件时,所有属性名和事件名都是子组件的,属性值 和 事件方法都是父组件

  1. vm.$on绑定事件 vm.$once绑定一次 vm.$off解绑事件 vm.$emit触发事件
  2. 给父亲绑定一些事件,儿子触发事件时将数据传递过去
  3. 单向数据流:父亲数据刷新,儿子数据就刷新
  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">
        <!-- 子传父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作用定制模板,用共同的模板插入不同的内容

  1. 没有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>
发布了57 篇原创文章 · 获赞 4 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Eva3288/article/details/104301712