在第五章中我们已经介绍了需要Vue内置的指令,比如v-if、v-show等,这些丰富的内置指令能满足我们的绝大部分的业务需求,不过在需要一些特殊功能时,我们仍然希望对DOM进行底层的操作,这个时候就需要用到自定义指令。
8.1基本用法
自定义指令的注册方法和组件很像,也分全局注册和局部注册,比如注册一个v-focus的指令,用于、
//全局注册
Vue.directive('focus',{
//指令选项
});
//局部注册
var app= new Vue({
el:'#app',
directives:{
focus:{
//指令选项
}
}
})
写法与组件基本类似,只是方法名由component改为了directive。上例值是注册了自定义指令v-focus,还没有实现具体功能,下面具体介绍自定义指令的各个选项。
自定义指令的选项是由几个钩子函数组成的,每个都是可选的。
bind:只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个在绑定时执行一次的初始化动作。
inserted:被绑定元素插入父节点时调用(父节点存在即可调用,不必存在与document中).
update:被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
unbind:指令只调用一次,指令与元素解绑时调用。
可以根据需求在不同的钩子函数内完成逻辑代码,例如上面的v-focus,我们希望在元素插入父节点时就调用,那用到的最好是inserted,示例代码如下:
Vue.directive('v-focus',{
inserted:function(el){
el.focus();//聚焦元素
}
});
var app = new Vue({
el:'#app'
})
每个钩子函数都有几个参数可用,比如上面我们用到了el。它们的含义如下:
el:指令所绑定的元素,可以用来直接操作DOM。
binding:一个对象,包含以下属性:
name 指令名,不包括v-前缀。
value 指令的绑定值,例如v-my-directive="1+1“,value的值是2.
oldValue 指令绑定的前一个值,仅在update和componentUpdated的钩子中可用,无论值是否改变都可用。
expression 绑定值的字符串形式,例如v-my-directive="1+1",expression的值是”1“。
arg 传给指令的参数,例如v-my-directive:foo,arg的值是foo。
modifiers 一个包含修饰符的对象,例如v-my-directive.foo.bar。修饰符对象modifiers的值是{foo:true,bar:true}.
vnode :Vue编译生成的虚拟节点,在进阶篇中介绍。
oldVnode:上一个虚拟节点,仅在update和componentUpdated钩子中使用。
下面是结合了以上参数的一个具体示例,代码如下:
Vue.directive('test',{
bind:function(){
var key = {};
for (var i in vnode){
key.push(i);
}
el.innerHTML =
'name' +biding.name +'
'+
'value' +biding.value+'
'+
'expression' +biding.expression+'
'+
'argument' +biding.arg+'
'+
'modifiers' +JSON.stringify(biding.modifiers) +'
'+
'vnode' +keys.join(',')
}
});
var app = new Vue({
el:'#app',
data:{
message:'some text'
}
})
执行后,
name:test
value:some text
expression:message
argument:msg
modifiers:{"a":true,"b":true}
vnode keys:
tag,data,children,text.elm,ns,context,functionalContext,key,componentOptions,componentInstance,parent,raw,isStatic,isRootInsert,isComment,isCloned,isOnce
在大多数场景,我们会在bind钩子里绑定一些事件,比如在document上用addEventListener绑定,在unbind里用removeEventListener解绑,比较典型的示例就是让这个元素随着鼠标拖曳。在后面的8.2章节中,我们会详细介绍到。
如果需要多个值,自定义指令也可以传入一个JavaScript对象字面量,只要是合法类型的JavaScript表达式都是可以的。示例代码如下:
Vue.directive('test',{
bind:function(el,binding,vnode){
console.log(binding.value.msg);
console.log(binding.value.name);
}
});
var app = new Vue({
el:'app'
})
Vue2.x移除了大量Vue1.x自定义指令的配置。在使用自定义指令时,应该充分理解业务需求,因为很多时候你需要的可能并不是自定义指令,而是组件。在下一节中,我们结合两个经典的示例在进一步了解自定义指令的使用场景和用法。
8.2实战
8.2.1开发一个可从外部关闭的下拉菜单
网页中有很多常见的下拉菜单。点击某个按钮会弹出一个下拉菜单,然后点击页面中其它空白区域(除了菜单本身外),菜单就关闭了。本示例就用自定义指令来实现这样的需求。
先来分析一下如何实现。
该示例有两个特点,一是下拉菜单本身是不会关闭的,二是点击下拉菜单以外的所以区域都要关闭。点击所有区域可以在document上绑定click事件来实现,同时只要过滤出是否点击的是目标元素内部的元素即可。
首先初始化各个文件:
index.html
可从外部关闭的下拉菜单
/
index.js
var app = new Vue({
el:'#app'
});
clickoutside.js
Vue.directive('clickoutside',{
});
利用组件的基本知识很容易完成index.html和index.js的逻辑:
下拉框的内容,点击外面区域可以关闭
var app = new Vue({
el:'#app',
data:{
show:false
},
methods:{
handleClose:{
this.show=false;
}
}
});
逻辑很简单,点击按钮时显示class为dropdown的div元素。
自定义指令v-clickoutside绑定了一个函数handleClose,原来关闭菜单。先来看一下clickoutside.js中的内容:
Vue.directive('clickoutside',{
bind:function(el,binding,vnode){
function doucumentHandler(e){
if(el.contains(e.target)){
return false;
}
if(binding.expression){
binding.value(e);
}
el._vueClickOutside_ = documentHandler;
document.addEventListener('click',documentHandler);
},
unbind:function(el,binding){
document.removeEventListener('click',el._vueClickOutside_);
delete el._vueClickOutside_;
}
}
});
之前分析过,要在document上绑定click事件,所以在bind钩子内声明了一个函数documentHandler,并将它作为句柄绑定在document的click事件上。documentHandler函数做了两个判断,第一个是判断点击的区域是否是指令所在的元素内部,如果是,就跳出函数,不往下继续执行。
TIPS:contains方法是用来判断元素A是否包含了元素B,包含返回true,不包含返回false,示例代码如下:
父元素
子元素
var A =document.getElementById('parent');
var B =document.getElementById('children');
console.log(A.contains(B));//true
console.log(B.contains(A));//false
第二个判断的是当前的指令v-clickoutside有没有写表达式,在该自定义指令中,表达式应该是一个函数,在过滤了内部元素后,点击外面任何区域应该执行用户表达式中的函数,所以binding.value()就是用来执行当前上下文methods中指定的函数的。
与Vue1.x不同的是,在自定义指令中,不能再用this.xxx的形式在上下文中声明一个变量。所以用el._vueClickOutside_引用了doucumentHandler,这样就可以在unbind钩子里移除对document的click事件监听。如果不移除,当组件或元素销毁时,它仍然存在于内存中。
以上代码分解完整代码基本一致,不再重复提供。下面是style.css的代码:
[v-cloak]{
display:none;
}
.main{
width:125px;
}
button{
display:block;
width:100%;
color:#fff;
background-color:#39f;
border:0;
padding:6px;
text-align:center;
font-size:12px;
border-radius:4px;
cussor:pointer;
outline:none;
position:relative;
}
button:active{
top:1px;
left:1px;
}
.dropdown{
width:100%;
height:150px;
margin:5px 0;
}
8.2.2开发一个实时事件转换指令v-time
在一些社区,比如微博、朋友圈等,发布的动态会有一个相对本机时间转换后的相对时间。(2小时前,11天前等).
一般在服务器的存储事件格式是Unix时间戳,比如2017-01-01 00:00:00的时间戳是1483200000.前端在拿到数据后,将它转换为可读的时间格式再显示出来。为了显出实时性,在一些社交类产品中,甚至会实时转换为几秒钟前、几分钟前、几小时前等不同的格式,这样比直接转换为年、月、日、时、分、秒更友好。本示例就来实现这样一个自定义指令v-time,将表达式传入的时间戳实时转换为相对时间。
便于演示效果,我们初始化时定义了两个时间。
index.html
时间转换指令
index.js
var app = new Vue({
el:'#app',
data:{
timeNow:(new Date().getTime(),
timeBefore:1488930695721
}
})
timeNow是目前的时间,timeBefore是一个写死的时间:2017-03-08.
TIP:本示例所用的时间戳都是毫秒级,如服务端返回秒级时间戳需要乘以1000后再使用。
分析一下时间转换逻辑:
1分钟以前,显示“刚刚”
1分钟-1小时之间,显示“xx分钟前”。
1小时-24小时之间,显示:"xx小时前“。
1天-一个月(31天)间,显示:"xx天前”。
大于1个月,显示“xx年xx月xx日”。
为了使判断逻辑更简单,统一使用时间戳进行时间大小判断。在写指令v-time之前,需要先写一系列与时间相关的函数,我们声明一个对象Time,把它们都封装在里面。
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分0秒的时间戳
getYearUnix:function(){
var date = new Date();
date.setMonth(0);
date.setDate(0);
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliSeconds(0);
return .date.getTime();
},
// 获取标准年月日
getYearUnix:function(){
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;
},
//转换时间
getFormatTime:function(){
var now = this.getUnix();//当前时间戳
var today=this.getTodayUnix();//今天0点时间戳
var year = this.getYearUnix();//今年0点时间戳
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.floor(timer/86400)+'天前';
}else{
tip =this.getLastDate(timetamp);
}
return tip;
}
};
Time.getFormatTime()方法就是自定义指令v-time所需要的,入参为毫秒级时间戳,返回已经整理号事件格式的字符串。
最后在time.js里补全生于代码:
Vue.directive('time',{
bind:function(el,binding){
el.innerHTML=Time.getFomatTime(binding.value);
el._timeout_=setInterval(function(){
el.innerHTML=Time.getFomatTime(binding.value);
},60000);
},
unbind:function(el){
clearInterval(el._timeout_);
delete el._timeout_;
}
});
在bind钩子里,将指令v-time表达式的值binding.value作为参数传入Time.getFormatTime()方法得到格式化时间,再通过el/innerHTML写入指令坐在元素。定时器el._timeout_每分钟出发一次,更新时间,并且在unbind钩子里清除掉。
总结:在编写自定义指令时,给DOM绑定一次性事件等初始条件,建议在bind钩子内完成。同时要在unbind钩子内解除相关绑定。在自定义指令里,理论上可以任意操作DOM,但这又违背了Vue.js的初衷,所以对于大幅度 DOM变动,应该使用组件。
上一章:vue.js组件详解(基础篇)
下一章:Render函数(进阶篇)-未更新