【Vue】Vue学习笔记——自定义指令&&组件


2. 自定义指令

2.1 指令的注册

自定义指令的注册分为全局注册和局部注册,例如注册一个v-focus指令用于< input >、< textarea >元素在页面加载时自动获取焦点。即只要打开这个页面后没单击任何内容,这个页面的输入框就应当处于聚焦状态。

语法:Vue.directive(id,definition)。id是指令是唯一标识,definition定义对象则是指令的相关属性及钩子函数。格式如下:

//注册一个全局自定义指令v-focus
Vue.directive('focus',{
//定义对象
})

也可以注册局部指令,组件或Vue构造函数中接受一个directive的选项,格式如下:

var vm=new Vue({
	el:"#app",
	directives:{
		focus:{
		//定义对象
		}
	}
})

2.2 指令的定义对象

我们可以传入definition定义对象从而对指令赋予具体的功能。

一个指令定义对象可以提供一下几个钩子函数:

  • bind:只调用一次,指令第一次绑定到元素时调用,在这里可以进行一次性初始化设置。
  • inserted:被绑定元素插入父结点时调用(父结点存在即可调用)。
  • update:被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后绑定的值,可以忽略不必要的模板更新。
  • componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
  • unbind:只调用一次,指令与元素解绑时调用。

根据需求在不同的钩子函数内完成逻辑代码,如上面的v-focus,我们希望元素插入父结点时就调用,比较好的钩子函数是inserted。

<p>页面载入时,input元素自动获取焦点</p>
<input v-focus>

<script>
    var vm=new Vue({
        el:"#app",
        data:{
            
        },
        directives:{
            //注册一个局部的自定义指令v-focus
            focus:{
                //指令的定义
                inserted:function(el){
                    //聚焦元素
                    el.focus();
                }
            }
        }
    })
</script>

一旦打开界面,input输入框就自动获得焦点,成为可输入状态。

2.3 指令实例属性

在指令的钩子函数中,可以 通过this来调用指令实例。

  1. el:指令所绑定的元素,可以直接操作DOM.
  2. binding:一个对象,包含以下property。
    1. name:指令名。
    2. value:指令的绑定值,例如v-my-directive="1+1"中绑定值为2.
    3. oldValue:指令绑定的前一个值,尽在update和componentUpdated中可用,无论值是否改变都可用。
    4. expression:字符串形式的指令表达式,例如v-my-directive="1+1"中表达式为“1+1”.
    5. arg:传给指令的参数,可选。例如例如v-my-directive:foo中参数为”foo“。
    6. modifiers:一个包含修饰符的对象。例如例如v-my-directive.foo.bar中,修饰符对象为{foo:true,bar:true}。
  3. vnode:Vue编译生成的虚拟节点。(VNode API查看更多)。
  4. oldVnode:上一个虚拟节点,仅在update和componentUpdated钩子中可用
<div v-demo="{color:'green',text:'自定义指令!'}"></div>

Vue.directive('demo',function(el,binding){
        //以简写的方式设置文本及背景颜色
        el.innerHTML=binding.value.text
        el.style.background=binding.value.color
    })

2.4 案例

2.4.1 下拉菜单

网页上很多常见的下拉菜单,单击“下拉”时会弹出下拉菜单。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="../vue.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
<div id="app">
    <!--自定义指令v-clickoutside绑定handleHide函数-->
<div class="main" v-clickoutside="handleHide">
    <button @click="show=!show">单击显示下拉菜单</button>
    <div class="dropdown" v-show="show">
        <div class="item"><a href="#">选项1</a></div>
        <div class="item"><a href="#">选项2</a></div>
        <div class="item"><a href="#">选项3</a></div>
    </div>
</div>
</div>

<script>
    //自定义指令v-clickoutside
    Vue.directive('clickoutside',{
        /*在document上绑定click事件,所以在bind钩子函数内声明一个函数
        documentHandler,并将它作为句柄绑定在document的click事件上
        documentHandler函数做了两个判断
        **/
        bind(el,binding){
            function documentHandler(e){
                /*第一个是判断单击的区域是否是指令所在的元素内部,如果是,就跳转函数不往下继续执行
                *contains方法用来判断元素A是否包含了元素B,包含则返回true
                **/
                if(el.contains(e.target)){
                    return false;
                }
                /*第二个是判断当前指令v-clickoutside有没有表达式,在该自定义指令中
                表达式应该是一个函数,在过滤了内部元素后,单击外部任何区域都应该执行用户表达式中的函数
                所以binding.value()用来执行当前上下文中methods中指定的函数
                **/
                if(binding.expression){
                    binding.value(e)
                }
            }
            el._vueMenuHandler_=documentHandler;
            document.addEventListener('click',el._vueMenuHandler_)
        },
        unbind(el){
            document.removeEventListener('click',el._vueMenuHandler_)
            delete el._vueMenuHandler_
        }
    })
    var vm=new Vue({
        el:"#app",
        data:{
            show:false
        },
        methods:{
            handleHide(){
                this.show=false
            }
        }
    })
</script>
</body>
</html>

效果如下所示:
在这里插入图片描述

2.4.2 相对时间转换

在很多社区网站,发布的动态都会有一个相对本机时间转换后的相对时间。

一般在服务器的存储时间格式是UNIX时间戳,例如2018-01-17 06:00:00的时间戳是1516140000。前端在得到数据后,将它转换为可持续的时间格式再显示出来。为了显示出实时性,在一些社交类产品中,甚至会实时转换为几秒前、几分钟前、几小时前等不同的格式,因为这样比直接转换为年月日时分秒显得对用户更加友好,体验更人性化。

我们来实现这样一个Vue自定义指令v-time,将表达式传入的时间戳实时转换为相对时间。

index.html代码如下:

<!DOCTYPE html>
<html>
<head>
    <title>时间转换指令</title>
    <meta charset="utf-8"/>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
    <div v-time="timeNow"></div>
    <div v-time="timeBefore"></div>
</div>
<script src="./time.js"></script>
<script src="./index.js"></script>
</body>
</html>

index.js代码如下:

var vm = new Vue({
    el: '#app',
    data: {
        timeNow: (new Date()).getTime(),
        timeBefore: 1580503571085
    }
})

time.js代码如下:

var Time = {
    //获取当前时间戳
    getUnix: function() {
        var date = new Date();
        return date.getTime();
    },

//获取今天0点0分0秒的时间戳
getTodayUnix: function() {
    var date = new Date();
    date.setHours(0);
    date.setMinutes(0);
    date.setSeconds(0);
    date.setMilliseconds(0);
    return date.getTime();
},

//获取今年1月1日0点0秒的时间戳
getYearUnix: function() {
    var date = new Date();
    date.setMonth(0);
    date.setDate(1);
    date.setHours(0);
    date.setMinutes(0);
    date.setSeconds(0);
    date.setMilliseconds(0);
    return date.getTime();
},
//获取标准年月日
getLastDate: function(time) {
    var date = new Date(time);
    var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1;
    var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
    return date.getFullYear() + '-' + month + '-' + day;
},
//转换时间
getFormateTime: function(timestamp) {
    var now = this.getUnix();
    var today = this.getTodayUnix();
    var year = this.getYearUnix();
    var timer = (now - timestamp) / 1000;
    var tip = '';

    if (timer <= 0) {
        tip = '刚刚';
    } else if (Math.floor(timer / 60) <= 0) {
        tip = '刚刚';
    } else if (timer < 3600) {
        tip = Math.floor(timer / 60) + '分钟前';
    } else if (timer >= 3600 && (timestamp - today >= 0)) {
        tip = Math.floor(timer / 3600) + '小时前';
    } else if (timer / 86400 <= 31) {
        tip = Math.ceil(timer / 86400) + '天前';
    } else {
        tip = this.getLastDate(timestamp);
    }
    return tip;
}
}
Vue.directive('time',{
    bind:function(el,binding){
        el.innerHTML=Time.getFormateTime(binding.value);
        el._timeout_=setInterval(()=>{
            el.innerHTML=Time.getFormateTime(binding.value);
        },60000);
    },
    unbind:function(){
        clearInterval(el._timeout_);
        delete el._timeout_;
    }
})

timeNow是当前时间,timeBefore是一个固定时间2020-02-01

时间转换逻辑:

  • 1分钟以前——刚刚
  • 1min~1h——xx分钟前
  • 1h~1d——xx小时前
  • 1d~1m——xx天前
  • 大于一个月——xx年xx月xx日

这样罗列出来逻辑就一目了然了。为了使判断更简单,我们这里统一使用时间戳进行大小判断。在写v-time指令之前需要写一系列与时间相关的函数,我们声明一个对象Time把它们都封装在里面。

Time.getFormatTime()方法就是自定义指令v-time中所需要的方法,参数为毫秒级时间戳,返回已经整理好的时间格式的字符串。

在bind钩子里,将指令v-time表达式的值binding.value作为参数传入Time.getFormatTime()方法中得到格式化时间,再通过el.innerHTML写入指令所在元素。定时器el._ timeout _ 每分钟触发一次,更新时间,并且在unbind钩子里清除掉。

总结:在编写自定义指令时,给DOM绑定一次性事件等初始动作,建议在bind钩子内完成,同时要在unbind内解除相关绑定。

时间转换结果:
在这里插入图片描述

3. 组件

3.1 什么是组件

组件是可复用的Vue实例,且带有一个名字,在这个示例中组件的名字是。可在一个通过new Vue创建的Vue实例中把这个组件作为自定义标签来使用。

    <button-counter></button-counter>
    <button-counter></button-counter>
    
        Vue.component('button-counter',{
        data:function(){
            return{
                count:0
            }
        },
        template:'<button v-on:click="count++">You clicked me {{count}} times.</button>'
    })

平时工作没见过的标签就是组件,每个标签代表着一个组件,这样就可以将组件进行任意次数的复用。

Web组件其实就是页面组成的一部分,好比计算机中的每一个硬件,它具有独立的逻辑和功能或界面,同时又能根据规定的接口规则进行相互融合,从而变成一个完整的应用。

Web页面就是由一个个类似这样的部分组成,例如:导航、列表、弹窗、下拉菜单等,页面只不过是这些组件的容器,组件自由组合形成功能完整的界面,当不需要某个组件或者想要替换某个组件时,可以随时进行替换和删除,而不影响整个应用的运行。

前端组件化的核心思路是将一个巨大且复杂的页面分成粒度合理的页面组成部分。

使用组件的好处:

  • 提高开发效率
  • 方便重复使用
  • 简化调试步骤
  • 提高整个项目的可维护性
  • 便于协同开发

3.2 组件的基本使用

为了能在模板中使用,这些组件必须先注册一遍Vue能够识别。

两种组件注册类型:全局注册、局部注册。

3.2.1 全局注册

全局注册需要确保在根实例初始化之前注册,这样才能使组件在任意实例中被使用。

使用Vue.component(tagName,options)注册。

<div id="app">
    <my-component></my-component>
</div>

<script>
	Vue.component('my-component',{
        template:'<h1>注册</h1>'
    });
    var vm=new Vue({
        el:"#app"
    })
</script>

注:

  • template的DOM结构必须被一个而且是唯一根元素所包含,直接引用而不被“< div >< /div >”包裹是无法被渲染的。
  • 模板声明了数据和最终展现给用户的DOM之间的映射关系。

除了template选项外,组件中还可以像Vue实例那样使用其他选项,例如data、computed、methods等,但是在使用data时和实例不同,data必须是函数然后将数据利用return返回。

<div id="app">
    <my-component></my-component>
</div>

<script>
	Vue.component('my-component',{
        template:'<h1>{{message}}</h1>',
        data:function(){
            return{
                message:'注册'
            }
        }
    });
    var vm=new Vue({
        el:"#app"
    })
</script>

Vue组件中data值不能为对象,因为对象是引用类型,组件可能会被多个实例同时引用。如果data值为对象,将导致多个实例共享一个对象,其中一个组件改变data属性值,其他实例也会受影响。

data为函数,通过return返回对象的复制,致使每个实例都有自己独立的对象,实例之间可以互不影响地改变data的属性值。

使用Vue.extend配合Vue.component方法:

<div id="app">
    <my-list></my-list>
</div>

<script>
	var list=Vue.extend({
        template:'<h1>this is a list</h1>',
    });
    Vue.component("my-list",list);
    new Vue({
        el:"#app"
    })
</script>

将模板字符串定义到script标签中,同时使用Vue.component定义组件。(script换成template也行)

<account></account>
<script id="tmpl" type="text/x-template">
    <div><a href="#">登录</a>|<a href="#">注册</a></div>
</script>
Vue.component('account',{
        template:'#tmpl'
    });

3.2.2 局部注册

如果不需要全局注册组件或者只要组件使用在其他组件内,可以采用选项对象components属性实现局部注册:

<account></account>
        components:{
            account:{
                template:'<div><h1>这是account组件</h1><login></login></div>',
                components:{
                    login:{
                        template:'<h3>这是登录组件</h3>'
                    }
                }
            }
        }

可以使用flag标识符结合v-if和v-else切换组件:

<account v-if="flag==true"></account>
<login v-else="flag==false"></login>

        components:{
            account:{
                template:'<div><h1>这是account组件</h1></div>',
            },
            login:{
                template:'<h3>这是登录组件</h3>'
            }
        }

3.2.3 DOM模板解析说明

当使用DOM作为模板时(例如,将el选项挂载到一个已存在的元素上),会受到HTML的一些限制,因为Vue只有在浏览器解析和标准化HTML后才能获取模板内容。尤其像这些元素< ul >、< ol >、< table >、< select >限制了能被它包裹的元素,而一些像< option >这样的元素只能出现在某些其他元素内部。

在自定义组件中使用这些受限制的元素会导致一些问题:

<table>
    <my-row>...</my-row>
</table>

自定义组件有时被认为是无效的内容,因此在渲染时会导致错误。这是因为使用特殊的js属性来挂载组件,代码如下:

<table>
    <tr is="my-row"></tr>
</table>

也就是说,在标准的HTML中,一些元素只能放置特定的子元素,而另一些元素只能存在于特定的父元素中。例如table中不能放置div,tr的父元素不能为div等。所以,当使用自定义标签时,标签名还是那些标签的名字,但是可以在标签的is属性中填写自定义组件的名字,如下所示:

<table border="1" cellpadding="5" cellspacing="0">
    <my-row></my-row>
    <tr is="my-row"></tr>
</table>

        components:{
            myRow:{
                template:'<tr><td>123456</td></tr>',
            }
        }

示例如下:
在这里插入图片描述
从图中可以发现直接引用组件标签并没有被

标签包裹,而用is特殊属性挂载的组件可以达到所需效果。

注:如果使用的是字符串模板,则不受限制。

3.3 组件选项

Vue组件可以理解为预先定义好行为的VueModel类。一个组件可以预先定义很多选项。但是核心的有以下几个:

  • 模板(template):模板声明了数据和最终展现给用户的DOM之间的映射关系。
  • 初始数据(data):一个组件的初始数据状态。对于可复用的组件来说,通常是私有的状态。
  • 接受的外部参数(props):组件之间通过参数来进行数据的传递和共享。参数默认是单向绑定(由上至下),但也可以是显式声明的双向绑定。
  • 方法(methods):对数据的改动操作一般都在组件的方法内进行。可以通过v-on指令将用户输入事件和组件方法进行绑定。
  • 生命周期钩子函数(lifecycle hooks):一个组件会触发多个生命周期钩子函数,例如created、attached、destroyed等。在这些钩子函数中,我们可以封装一些自定义的逻辑。和传统的MVC相比可以理解为controller的逻辑被分散到这些钩子函数中。

3.3.1 组件props

组件中更重要的功能是组件间进行通信,选项props是组件中非常重要的一个选项,起到父子组件之间桥梁的作用。

静态props

组件实例的作用域是孤立的,这意味着不能在子组件的模板内直接引用父组件的数据。

要想让子组件使用父组件的数据,需要通过子组件的props选项实现。子组件要显式地用props选项声明它期望获得的数据:

<my-component message="来自父组件的数据"></my-component>

    Vue.component('my-component',{
        //声明props
        props:['message'],
        //就像data一样,props可以用在模板内
        //同样也可以在vm实例中像“this.message”这样使用
        template:'<span>{{message}}</span>'
    })

由于HTML的特性不区分大小写,所以当使用的不是字符串模板时,camelCased(驼峰式)命名的props需要转换为相应的kebab-case(短横线隔开式)命名:

<my-component my-message="来自父组件的数据"></my-component>

    Vue.component('my-component',{
        props:['myMessage'],
        template:'<span>{{myMessage}}</span>'
    })

动态props

在模板中,有时候传递的数据并不一定是固定不变的,而是要动态绑定父组件的数据到子模版的props,与绑定到任何普通的HTML特性相类似,采用v-bind进行绑定。当父组件的数据发生变化时,该变化也会传递给子组件:

<input type="text" v-model="parentMessage">
<my-component :message="parentMessage"></my-component>

<script>
    Vue.component('my-component',{
        //声明props
        props:['message'],
        //就像data一样,props可以用在模板内
        //同样也可以在vm实例中像“this.message”这样使用
        template:'<span>{{message}}</span>'
    })
    var vm=new Vue({
        el:"#app",
        data:{
            parentMessage:''
        }
    })
</script>

注:如果在父组件中直接传递数字、布尔值、数组、对象,则它传递的值均为字符串。如果想传递一个实际的值,需使用v-bind,从而使它被当作JavaScript表达式进行计算:

<my-component :message="1+1"></my-component><br>
<my-component message="1+1"></my-component>

在这里插入图片描述

3.3.2 props验证

前面介绍的props选项的值都是数组,除了数组还可以是对象。可以为组件的props指定验证规则,如果传入的数据不符合规则,则Vue会发出警告。当props需要验证时,需要采用对象写法。

当组件给他人使用时推荐进行数据验证:

Vue.component('example',{
	props:{
		//基础类型检测,null的意思是任何类型都可以
		propA:Number,
		//多种类型
		propB:[String,Number],
		//必传且是字符串
		propC:{
			type:String,
			required:true
		},
		//有数字,默认值
		propD:{
			type:Number,
			default:100
		},
		//数组/对象的默认值应当由一个工厂函数返回
		propE:{
			type:Object,
			default:function(){
				return{message:'hrllo'}
			}
		},
		//自定义验证函数
		propF:{
			validator:function(value){
				return value>10
			}
		}
	}
})

当props验证失败时,Vue会抛出警告。props会在组件实例创建之前进行校验,所以在default或validator函数里,诸如data、computed或methods等实例属性还无法使用。

3.3.3 单向数据流

所有的props都使得其父子props之间形成了一个单向下行绑定:父级props的更新会向下流动到子组件中,但是反过来不行。之所以这样设计,是尽可能将父子组件解耦,避免子组件无意间修改了父组件的状态。

额外的,每次父组件发生变更时,子组件中所有的props都将刷新为最新的值。这意味着不应该在一个子组件内部改变props。如果这样做了,Vue会在浏览器的控制台发出警告。

<div id="example">
<parent></parent>
</div>
    
<script>
    var childNode = {
            template:
        '<div class="child"><div><span>子组件数据</span>' +
                ' <input v-model="childMsg"> </div> <p>{{childMsg}}</p></div>' ,
        props:['childMsg']
    }
    var parentNode = {
            template:
        '<div class="parent"><div><span>父组件数据</span>' +
                ' <input v-model="msg"> </div> <p>{{msg}}</p> <child :child-msg="msg"></child></div>',
        components: {
            'child': childNode
        },
        data(){
        return {
            'msg':'match'
        }
    }
    };
    // 创建根实例
    new Vue({
        el: '#example',
        components: {
            'parent': parentNode
        }
    })
</script>

在这里插入图片描述
父组件数据发生变化时,子组件数据也会相应变化,而子组件数据发生变化时,父组件数据保持不变,并在控制台发出警告。

业务中经常会遇到需要修改props中数据的情况,通常有以下两种原因:

  • props作为初始值传入后,子组件想把它当作局部数据来使用。
  • props作为初始值传入,由子组件处理成其他数据并输出。

注:JS中对象和数组是引用类型,指向同一个内存空间,如果props是一个对象或数组,在子组件内部改变它会影响父组件的状态。

对于这两种情况,正确的应对方式是:

  • 定义一个局部变量,并用props的值初始化它:

    props:['initialCounter'],
    data:function(){
    	return{counter:this.initialCounter}
    }
    

    但是,定义的局部变量counter只能接受initialCounter的初始值,当父组件要传递的值发生变化时,counter无法接受到最新值。

    <div id="example">
        <parent></parent>
    </div>
    
    <script>
        var childNode = {
                template:
            '<div class="child"><div><span>子组件数据</span>' +
                    ' <input v-model="temp"> </div> <p>{{temp}}</p></div>' ,
            props:['childMsg'],
            data(){
                return{
                    temp:this.childMsg
                }
            }
        }
        var parentNode = {
                template:
            '<div class="parent"><div><span>父组件数据</span>' +
                    ' <input v-model="msg"> </div> <p>{{msg}}</p> <child :child-msg="msg"></child></div>',
            components: {
                'child': childNode
            },
            data(){
            return {
                'msg':'match'
            }
        }
        };
        // 创建根实例
        new Vue({
            el: '#example',
            components: {
                'parent': parentNode
            }
        })
    </script>
    

    在示例中,除初始值外,父组件的值无法更新到子组件中。

  • 定义一个计算属性,处理props的值并返回:

    props:['size'],
    computed:{
    	normalizedSize:function(){
    		return this.size.trim().toLowerCase()
    	}
    }
    

但是由于是计算属性,因此只能显示值,不能设置值

    var childNode = {
            template:
        '<div class="child"><div><span>子组件数据</span>' +
                ' <input v-model="temp"> </div> <p>{{temp}}</p></div>' ,
        props:['childMsg'],
        computed:{
            temp(){
                return this.childMsg
            }
        } 
    }

示例中,由于子组件使用的是计算属性,所以子组件的数据无法手动修改。

  • 更加妥帖的方案是,使用变量存储props的初始值,并使用watch观察props的值的变化。当props的值发生变化时,更新变量的值:

    <body>
        <div id="example">
            <parent></parent>
        </div>
        
        <script>
            var childNode = {
                    template:
                '<div class="child"><div><span>子组件数据</span>' +
                        ' <input v-model="temp"> </div> <p>{{temp}}</p></div>' ,
                props:['childMsg'],
                data(){
                    return{
                        temp:this.childMsg
                    }
                },
                watch:{
                    childMsg(){
                        this.temp=this.childMsg
                    }
                }
            }
            var parentNode = {
                    template:
                '<div class="parent"><div><span>父组件数据</span>' +
                        ' <input v-model="msg"> </div> <p>{{msg}}</p> <child :child-msg="msg"></child></div>',
                components: {
                    'child': childNode
                },
                data(){
                return {
                    'msg':'match'
                }
            }
            };
            // 创建根实例
            new Vue({
                el: '#example',
                components: {
                    'parent': parentNode
                }
            })
        </script>
    </body>
    

3.4 组件通信

在Vue组件通信中最常见的通信方式就是父子组件之间的通信。而父子组件的设定方式在不同情况下又各不相同,归纳起来,组件之间的通信方式如下图所示。最常见的就是父组件为控制组件而子组件为视图组件。父组件传递数据给子组件使用,遇到业务逻辑操作时子组件触发父组件的自定义事件。
在这里插入图片描述
组件关系有三种:父→子、子→父、非父子。

父组件向子组件的通信是通过props传递数据的,就好像方法的传参一样,父组件调用子组件并传入数据,子组件接收父组件传递的数据进行验证后使用。

3.4.1 自定义事件

当子组件向父组件传递数据时就需要用到自定义事件。v-on指令除了监听DOM事件外,还可以用子组件之间的自定义事件。

在子组件中用$emit()来触发事件以便将内部的数据传递给父组件:

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <meta charset="utf-8"/>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
    <my-component v-on:myclick="onClick"></my-component>
</div>
<script>
    Vue.component('my-component', {
        template:'<div>' +
            '<button type="button" @click="childClick">点击我触发自定义事件</button></div>' ,
        methods: {
            childClick () {
                this.$emit('myclick', '这是我传递出去的数据', '这是我传递出去的数据2')
            }
        }
    })
    new Vue({
        el: '#app',
        methods: {
            onClick () {
                console.log(arguments)
            }
        }
    })
</script>
</body>
</html>

上面示例中代码执行步骤:

  • 子组件在自己的方法中将自定义事件及需要传递的数据通过以下代码传递出去:

    this.$emit('myclick', '这是我传递出去的数据', '这是我传递出去的数据2')
    
    • 第一个参数是自定义的事件名
    • 后面的参数是依次想要传递出去的数据
  • 父组件利用v-on为事件绑定处理器,代码如下:

    <my-component v-on:myclick="onClick"></my-component>
    

    这样,在Vue实例的methods方法中就可以调用传进来的参数了。

3.4.2 $ emit/$ on

这种方法通过一个空的Vue实例作为中央事件中心,用它来触发事件和监听事件,巧妙而轻量地实现了任何组件之间的通信,包括父子、兄弟、跨级。

实现方式如下:

var Event=new Vue();
	Event.$emit(事件名,数据);
	Event.$on(事件名,data=>());

假设兄弟组件有三个,分别为A、B、C组件,C组件如何获取A组件或B组件的数据。

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <meta charset="utf-8"/>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
    <my-a></my-a>
    <my-b></my-b>
    <my-c></my-c>
</div>
<template id="a">
    <div>
        <h3>A组件:{{name}}</h3>
        <button @click="send">将数据发送给C组件</button>
    </div>
</template>
<template id="b">
    <div>
        <h3>B组件:{{age}}</h3>
        <button @click="send">将数组发送给C组件</button>
    </div>
</template>
<template id="c">
    <div>
        <h3>C组件:{{name}},{{age}}</h3>
    </div>
</template>
<script>
    var Event = new Vue();//定义一个空的Vue实例
    var A = {
        template: '#a',
        data() {
        return {
            name: 'beixi'
        }
    },
        methods: {
            send() {
                Event.$emit('data-a', this.name);
            }
        }
    }
    var B = {
        template: '#b',
        data() {
        return {
            age: 18
        }
    },
        methods: {
            send() {
                Event.$emit('data-b', this.age);
            }
        }
    }
    var C = {
        template: '#c',
        data() {
        return {
            name: '',
                age: ""
    }
    },
        mounted() {//在模板编译完成后执行
        Event.$on('data-a',name => {
            this.name = name;//箭头函数内部不会产生新的this,这边如果不用=>,this指代Event
        })
        Event.$on('data-b',age => {
            this.age = age;
        })
    }
    }
    var vm = new Vue({
        el: '#app',
        components: {
            'my-a': A,
            'my-b': B,
            'my-c': C
        }
    });
</script>
</body>
</html>

页面效果如下:
在这里插入图片描述
$on监听了自定义事件data-a和data-b,因为有时不确定何时会触发事件,一般会在mounted和created钩子中进行监听。

3.5 内容分发

在实际项目开发中,时常会把父组件的内容与子组件自己的模板混合起来使用。而这样的一个过程在Vue中被称为内容分发,也常常被称为slot(插槽)。其主要参照了当前web components规范草案,使用特殊的< slot >元素作为原始内容的插槽。

3.5.1 基础用法

由于slot是一块模板,因此对于任何一个组件,从模板种类的角度来分,其实都可以分为非插槽模板和插槽模板。其中非插槽模板是HTML模板(也就是HTML的一些元素,例如div、span等元素),其显示与否及怎样显示完全由插件自身控制,但插槽模板(也就是slot)是一个空壳子,它显示与否及怎样显示完全由父组件来控制。不过,插槽显示的位置由子组件自身决定,slot写在组件template的哪部分,父组件传过来的模板将来就显示在哪部分。

一般定义子组件的代码如下:

<div id="app">
<child>
    <span>123456</span>
</child>
</div>

<script>
    var vm=new Vue({
        el:"#app",
        data:{
            
        },
        components:{
            child:{
                template:'<div>这是子组件内容</div>'
            }
        }
    })
</script>

页面 显示结果:这是子组件内容。< span >123456< /span >内容不会显示。虽然< span >标签被子组件的child标签所包含,但由于它不在子组件的template属性中,因此不属于子组件。

在template中添加slot标签:

template:'<div><slot></slot>这是子组件内容</div>'

页面显示结果:123456这是子组件内容.

分布解析内容分发:

  • 假设有一个组件名为my-component,其使用上下文代码如下:

    <my-component>
    	<p>
            hi,slots
        </p>
    </my-component>
    
  • 再假设此组件模板为:

    <div>
        <slot></slot>
    </div>
    
  • 那么注入后的组件HTML相当于:

    <div>
        <p>
            hi,slots
        </p>
    </div>
    

标签< slot >会把组件使用上下文的内容注入此标签所占据的位置。组件分发的概念简单而强大,因为它意味着对一个隔离的组件除了通过属性、事件交互之外,还可以注入内容。

  • 传入简单数据——property
  • 传入js表达式或对象——事件
  • 传入HTML标签——内容分发

3.5.2 编译作用域

在深入了解分发API之前,先明确内容在哪个作用域里编译,假定模板为:

<child-component>
	{{message}}
</child-component>

这里的message就是一个slot,绑定的是父组件的数据。,而不是子组件< child-component >的数据。

组件作用域简单来说就是父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。

<div id="app">
      <child-component  v-show="someChildProperty"></child-component>
</div>
<script>
    Vue.component('child-component', {
        template: '<div>这是子组件内容</div>',
        data: function () {
            return {
                someChildProperty: true
            }
        }
    })
    new Vue({
        el:'#app'
    })
</script>

这里someChildProperty绑定的是父组件的数据,所以是无效的,因此获取不到数据。如果想在子组件中绑定,可以采用如下代码:

<child-component></child-component>
template: '<div v-show="someChildProperty">这是子组件内容</div>'

因此,slot分发的内容是在父作用域内进行编译的。

3.5.3 默认slot

如果要使父组件在子组件中插入内容,必须在子组件中声明slot标签,如果子组件模板不包含< slot >插口,父组件的内容将会被丢弃。

3.5.4 具名slot

slot元素可以用一个特殊的属性name来配置分发内容。多个slot标签可以有不同的名字。

使用方法如下:

  • 父组件要在分发的标签中添加属性“slot=name名”。
  • 子组件在对应分发位置上的slot标签中添加属性“name=name名”。
<div id="app">
    <child>
    <span slot="one">123456</span>
    <span slot="two">abcdef</span>
    </child>
    </div>
<script>
    new Vue({
        el:'#app',
        components:{
            child:{
                template:"<div><slot name='two'></slot>我是子组件<slot name='one'></slot></div>"
            }
        }
    });
</script>

slot分发其实就是父组件在子组件内放一些DOM,它负责这些DOM是否显示以及在哪个地方显示。

3.5.5 作用域插槽

插槽分为单个插槽、具名插槽和作用域插槽。

前两种插槽的内容和样式皆由父组件决定,也就是说显示什么内容和怎样显示都由父组件决定,而作用域插槽的样式仍由父组件决定,但内容由子组件控制。即前两种插槽不能绑定数据,而作用域插槽是一个带绑定数据的插槽。

作用域插槽更具代表性的应用是列表组件,允许组件自定义如何渲染列表的每一项:

<div id="app">
<child>
    <template slot-scope="props"><!--固定写法,属性值可以自定义-->
        <li>{{props.item}}</li><!--插值表达式可以直接使用-->
    </template>
</child>
</div>

<script>
    Vue.component('child',{
        data(){
            return{
                list:[1,2,3,4]
            }
        },
        template:'<div><ul>'+
            '<slot v-for="item in list" :item=item></slot></ul></div>',
    })
    var vm=new Vue({
        el:"#app",
        data:{
            
        }
    })
</script>

< slot v-for=“item in list” :item=item >< /slot >这段代码的意思是child组件去实现一个列表的循环,但是列表项中的每一项怎样显示并不用关心,具体怎样显示由外部决定。

< template slot-scope=“props” >< /template >是一个固定写法,属性值可以自定义,它的意思是当子组件用slot时会向子组件传递一个item,子组件接收的数据都放在props上。

当子组件循环或某一部分的DOM结构应该由外部传递进来时,我们要用作用域插槽。使用作用域插槽,子组件可以向父组件的作用域插槽传递数据,父组件如果想接收这个数据,必须在外层使用template模板占位符,同时通过slot-scope对应的属性名字接收传递过来的数据。如上面代码,传递一个item过来,在父组件的作用域插槽中就可以接收到这个item,然后就可以使用它了。

3.6 动态组件

让多个组件使用同一个挂载点,并动态切换,这就是动态组件。通过使用保留的元素,动态地绑定它的js特性,可以实现动态组件。往往应用在路由控制或者tab切换中。

3.6.1 基本用法

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <meta charset="utf-8"/>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
    <button @click="change">切换页面</button>
    <component :is="currentView"></component>
</div>
<script>
    new Vue({
        el: '#app',
        data:{
            index:0,
            arr:[
                {template:'<div>我是主页</div>'},
                {template:'<div>我是提交页</div>'},
                {template:'<div>我是存档页</div>'}
            ],
    },
        computed:{
            currentView(){
                return this.arr[this.index];
            }
        },
        methods:{
            change(){
                this.index = (++this.index)%3;
            }
        }
    })
</script>
</body>
</html>

< component >标签中is属性决定了当前采用的子组件,:is是v-bind的简写,绑定了父组件中data的currentView属性。单击按钮时,会更改数组arr的索引值,同时也修改了子组件的内容。

3.6.2 keep-alive

动态切换掉的组件(非当前显示的组件)被移除掉了,如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。< keep-alive >包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们,以便提高提取效率。和< transition >相似,< keep-alive >是一个抽象组件,它自身不会渲染一个DOM元素,也不会出现在父组件链中。

<div id="app">
    <button @click="change">切换页面</button>
    <keep-alive>
        <component :is="currentView"></component>
    </keep-alive>
</div>

如果有多个条件性的子元素,< keep-alive >要求同时只有一个子元素被渲染时可以使用条件语句进行判断。

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <meta charset="utf-8"/>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
    <button @click="change">切换页面</button>
    <keep-alive>
        <home v-if="index===0"></home>
        <posts v-else-if="index===1"></posts>
        <archive v-else></archive>
    </keep-alive>
</div>
<script>
    new Vue({
        el: '#app',
        components:{
            home:{template:'<div>我是主页</div>'},
                posts:{template:'<div>我是提交页</div>'},
                    archive:{template:'<div>我是存档页</div>'},
                    },
        data:{
            index:0,
        },
        methods:{
            change(){
                //  在data外面定义的属性和方法通过$options可以获取和调用
                let len = Object.keys(this.$options.components).length;
                this.index = (++this.index)%len;
            }
        }
                })
</script>
</body>
</html>

3.6.3 activated钩子函数

Vue给组件提供了activated钩子函数,作用与动态组件切换或静态组件初始化的过程中。activated是和template、data等属性平级的一个属性,其形式是一个函数,函数里默认只有一个参数,而这个参数是一个函数,执行这个函数时,才会切换组件,即可延迟执行当前的组件。

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <meta charset="utf-8"/>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
    <button @click='toShow'>点击显示子组件</button>
    <!----或者<component v-bind:is="which_to_show" keep-alive></component>也行----->
    <keep-alive>
        <component v-bind:is="which_to_show" ></component>
    </keep-alive>
</div>
<script>
    // 创建根实例
    var vm = new Vue({
        el: '#app',
        data: {
            which_to_show: "first"
        },
        methods: {
            toShow: function () {   //切换组件显示
                var arr = ["first", "second", "third", ""];
                var index = arr.indexOf(this.which_to_show);
                if (index < 2) {
                    this.which_to_show = arr[index + 1];
                } else {
                    this.which_to_show = arr[0];
                }
                console.log(this.$children);
            }
        },
        components: {
            first: { //第一个子组件
                template: "<div>这里是子组件1</div>"
            },
            second: { //第二个子组件
                template: "<div>这里是子组件2,这里是延迟后的内容:{{hello}}</div>",
                data: function () {
                    return {
                        hello: ""
                    }
                },
                activated: function (done) { //执行这个参数时,才会切换组件
                    console.log('beixi')
                    var self = this;
                    var startTime = new Date().getTime(); // get the current time
                    //两秒后执行
                    while (new Date().getTime() < startTime + 2000){
                        self.hello='我是延迟后的内容';
                    }
                }
            },
            third: { //第三个子组件
                template: "<div>这里是子组件3</div>"
            }
        }
    });
</script>
</body>
</html>

当切换到第二个组件的时候,会先执行activated钩子函数,会在2s后显示组件2,起到了延迟加载的作用。

3.6.4 综合案例

添加组件后,通过单击“发表评论”按钮来添加内容并传递到评论列表中。实现的逻辑是:

  • 通过单击“发表评论”按钮触发单击事件并调用组件中methods所定义的方法。
  • 在methods所定义的方法中加载并保存localStorage的列表数据到list中。
  • 将录入的信息添加到list中,然后将数据保存在localStorage中。
  • 调用父组件的方法来刷新列表数据。
<!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>
    <!--引入vue-->
    <script src="/vue.js"></script>
    <!--引入bootstrap-->
    <link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css">
</head>
<body>
<div id="app">
    <cmt-box @func="loadComments"></cmt-box>
    <ul class="list-group">
        <li class="list-group-item" v-for="item in list" :key="item.id">
            <span class="badge">评论人: {{ item.user }}</span>
            {{ item.content }}
        </li>
    </ul>
</div>
<template id="tmpl">
    <div>
        <div class="form-group">
            <label>评论人:</label>
            <input type="text" class="form-control" v-model="user">
        </div>
        <div class="form-group">
            <label>评论内容:</label>
            <textarea class="form-control" v-model="content"></textarea>
        </div>
        <div class="form-group">
            <input type="button" value="发表评论" class="btn btn-primary" @click="postComment">
        </div>
    </div>
</template>
<script>
    var commentBox = {
        data() {
            return {
                user: '',
                content: ''
        }
        },
        template: '#tmpl',
        methods: {
            postComment() { // 发表评论的方法
                var comment = { id: Date.now(), user: this.user, content: this.content }
                // 从 localStorage 中获取所有的评论
                var list = JSON.parse(localStorage.getItem('cmts') || '[]')
                list.unshift(comment)
                // 重新保存最新的 评论数据
                localStorage.setItem('cmts', JSON.stringify(list))
                this.user = this.content = ''
                this.$emit('func')
            }
        }
    }
    // 创建 Vue 实例,得到 ViewModel
    var vm = new Vue({
        el: '#app',
        data: {
            list: [
                { id: Date.now(), user: 'beixi', content: '这是我的网名' },
                { id: Date.now(), user: 'jzj', content: '这是我的真名' },
                { id: Date.now(), user: '贝西奇谈', content: '有任何问题可以关注公众号' }
        ]
        },
        beforeCreate(){ /* 注意:这里不能调用 loadComments 方法,因为在执行这个钩子函数的时候,data 和 methods 都还没有被初始化好*/
    },
        created(){
        this.loadComments()
    },
        methods: {
            loadComments() { // 从本地的 localStorage 中,加载评论列表
                var list = JSON.parse(localStorage.getItem('cmts') || '[]')
                this.list = list
            }
        },
        components: {
            'cmt-box': commentBox
        }
    });
</script>
</body>
</html>

显示效果如下:
在这里插入图片描述

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值