第八章 vue.js-自定义指令(基础篇)

        在第五章中我们已经介绍了需要Vue内置的指令,比如v-if、v-show等,这些丰富的内置指令能满足我们的绝大部分的业务需求,不过在需要一些特殊功能时,我们仍然希望对DOM进行底层的操作,这个时候就需要用到自定义指令。

8.1基本用法

        自定义指令的注册方法和组件很像,也分全局注册和局部注册,比如注册一个v-focus的指令,用于<input>、<textarea>元素初始化时自动获得焦点,两种写法分别是:

//全局注册

Vue.directive('focus',{

    //指令选项

});

//局部注册

var app= new Vue({

        el:'#app',

        directives:{

        focus:{

            //指令选项

        }

    }

})

        写法与组件基本类似,只是方法名由component改为了directive。上例值是注册了自定义指令v-focus,还没有实现具体功能,下面具体介绍自定义指令的各个选项。

        自定义指令的选项是由几个钩子函数组成的,每个都是可选的。

      bind:只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个在绑定时执行一次的初始化动作。

        inserted:被绑定元素插入父节点时调用(父节点存在即可调用,不必存在与document中).

        update:被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。

        componentUpdated:被绑定元素所在模板完成一次更新周期时调用。

        unbind:指令只调用一次,指令与元素解绑时调用。

        可以根据需求在不同的钩子函数内完成逻辑代码,例如上面的v-focus,我们希望在元素插入父节点时就调用,那用到的最好是inserted,示例代码如下:

<div id="app">

        <input type="text" v-focus>

</div>

<script>

    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钩子中使用。

    下面是结合了以上参数的一个具体示例,代码如下:

<div id="app">

    <div v-test:msg.a.b="message"></div>

</div>

<script>

    Vue.directive('test',{

        bind:function(){

            var key = {};

            for (var i in vnode){

                key.push(i);

            }

            el.innerHTML =

            'name' +biding.name +'<br>'+

            'value' +biding.value+'<br>'+

            'expression' +biding.expression+'<br>'+

            'argument' +biding.arg+'<br>'+

            'modifiers' +JSON.stringify(biding.modifiers) +'<br>'+

            'vnode' +keys.join(',')

        }

    });

    var app = new Vue({

    el:'#app',

    data:{

        message:'some text'

    }

})

</script>

执行后,<div>的内容会使用inner HTML重置,结果为:

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表达式都是可以的。示例代码如下:

<div id="app">

    <div v-test="{msg:'hello',name:'Lmz'}"></div>

</div>

<script>

    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

<!DOCTYPE html>

<html>

<head>

    <meta charset="utf-8">

    <title>可从外部关闭的下拉菜单</title>

    <lin rel="stylesheet" type="text/css" href="style.css">/

</head>

<body>

    <div id="app" v-cloak></div>

    <script src="https:unpkg.com/vue/dist/vue.min.js"></script>

    <script src="clickoutside.js></script>

    <script src="index.js></script>

</body>

</html>

index.js

var app = new Vue({

    el:'#app'

});

clickoutside.js

    Vue.directive('clickoutside',{

});

利用组件的基本知识很容易完成index.html和index.js的逻辑:

<div id="app" v-cloak">

    <div class="main" v-clickoutside="handleClose">

        <button @click="show =!show">点击显示下拉菜单</button>

        <div class="dropdown" v-show="show">

                <p>下拉框的内容,点击外面区域可以关闭</p>

        </div>

    </div>

</div>

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,示例代码如下:

<body>

    <div id="parent">

            父元素

            <div id="children">子元素</div>

    </div>

    <script type="text/javascript">

    var A =document.getElementById('parent');

    var B =document.getElementById('children');

    console.log(A.contains(B));//true

    console.log(B.contains(A));//false

    </script>

</body>

        第二个判断的是当前的指令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

<!DOCTYPE html>

<html>

<head>

    <meta charset="utf-8">

    <title>时间转换指令</title>

</head>

<body>

    <div id="app" v-cloak>

        <div v-time="timeNow"></div>

        <div v-time="timeBefore"></div>

    </div>

    <script src = "https://unpkg.com/vue/dist/vue.min.js"></script>

    <srript src="time.js"></script>

    <script src="index.js"></script>

</body>

</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函数(进阶篇)-未更新

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容