? ? ? ? 在第五章中我們已經(jīng)介紹了需要Vue內(nèi)置的指令内贮,比如v-if宣赔、v-show等,這些豐富的內(nèi)置指令能滿足我們的絕大部分的業(yè)務(wù)需求侣姆,不過在需要一些特殊功能時,我們?nèi)匀幌M麑OM進行底層的操作沉噩,這個時候就需要用到自定義指令捺宗。
8.1基本用法
? ? ? ? 自定義指令的注冊方法和組件很像,也分全局注冊和局部注冊川蒙,比如注冊一個v-focus的指令蚜厉,用于<input>、<textarea>元素初始化時自動獲得焦點畜眨,兩種寫法分別是:
//全局注冊
Vue.directive('focus',{
? ? //指令選項
});
//局部注冊
var app= new Vue({
? ? ? ? el:'#app',
? ? ? ? directives:{
? ? ? ? focus:{
? ? ? ? ? ? //指令選項
? ? ? ? }
? ? }
})
? ? ? ? 寫法與組件基本類似昼牛,只是方法名由component改為了directive。上例值是注冊了自定義指令v-focus,還沒有實現(xiàn)具體功能康聂,下面具體介紹自定義指令的各個選項贰健。
? ? ? ? 自定義指令的選項是由幾個鉤子函數(shù)組成的,每個都是可選的恬汁。
? ? ? bind:只調(diào)用一次伶椿,指令第一次綁定到元素時調(diào)用,用這個鉤子函數(shù)可以定義一個在綁定時執(zhí)行一次的初始化動作氓侧。
? ? ? ? inserted:被綁定元素插入父節(jié)點時調(diào)用(父節(jié)點存在即可調(diào)用脊另,不必存在與document中).
? ? ? ? update:被綁定元素所在的模板更新時調(diào)用,而不論綁定值是否變化约巷。通過比較更新前后的綁定值偎痛,可以忽略不必要的模板更新。
? ? ? ? componentUpdated:被綁定元素所在模板完成一次更新周期時調(diào)用独郎。
? ? ? ? unbind:指令只調(diào)用一次踩麦,指令與元素解綁時調(diào)用。
? ? ? ? 可以根據(jù)需求在不同的鉤子函數(shù)內(nèi)完成邏輯代碼囚聚,例如上面的v-focus靖榕,我們希望在元素插入父節(jié)點時就調(diào)用,那用到的最好是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'
? ? })
? ? ? ? 每個鉤子函數(shù)都有幾個參數(shù)可用茁计,比如上面我們用到了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 傳給指令的參數(shù),例如v-my-directive:foo,arg的值是foo爷耀。
? ? ? ? ? ? modifiers 一個包含修飾符的對象甘桑,例如v-my-directive.foo.bar。修飾符對象modifiers的值是{foo:true,bar:true}.
? ? ? ? vnode :Vue編譯生成的虛擬節(jié)點歹叮,在進階篇中介紹跑杭。
? ? ? ? oldVnode:上一個虛擬節(jié)點,僅在update和componentUpdated鉤子中使用咆耿。
? ? 下面是結(jié)合了以上參數(shù)的一個具體示例德谅,代碼如下:
<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>
執(zhí)行后,<div>的內(nèi)容會使用inner HTML重置票灰,結(jié)果為:
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
? ? ? ? 在大多數(shù)場景女阀,我們會在bind鉤子里綁定一些事件,比如在document上用addEventListener綁定屑迂,在unbind里用removeEventListener解綁浸策,比較典型的示例就是讓這個元素隨著鼠標拖曳。在后面的8.2章節(jié)中惹盼,我們會詳細介紹到庸汗。
? ? ? ? 如果需要多個值,自定義指令也可以傳入一個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自定義指令的配置。在使用自定義指令時掩蛤,應(yīng)該充分理解業(yè)務(wù)需求枉昏,因為很多時候你需要的可能并不是自定義指令,而是組件揍鸟。在下一節(jié)中兄裂,我們結(jié)合兩個經(jīng)典的示例在進一步了解自定義指令的使用場景和用法。
8.2實戰(zhàn)
8.2.1開發(fā)一個可從外部關(guān)閉的下拉菜單
? ? ? ? 網(wǎng)頁中有很多常見的下拉菜單。點擊某個按鈕會彈出一個下拉菜單晰奖,然后點擊頁面中其它空白區(qū)域(除了菜單本身外)谈撒,菜單就關(guān)閉了。本示例就用自定義指令來實現(xiàn)這樣的需求匾南。
? ? ? ? 先來分析一下如何實現(xiàn)啃匿。
? ? ? ? 該示例有兩個特點,一是下拉菜單本身是不會關(guān)閉的蛆楞,二是點擊下拉菜單以外的所以區(qū)域都要關(guān)閉溯乒。點擊所有區(qū)域可以在document上綁定click事件來實現(xiàn),同時只要過濾出是否點擊的是目標元素內(nèi)部的元素即可臊岸。
? ? ? ? 首先初始化各個文件:
index.html
<!DOCTYPE html>
<html>
<head>
? ? <meta charset="utf-8">
? ? <title>可從外部關(guān)閉的下拉菜單</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>下拉框的內(nèi)容橙数,點擊外面區(qū)域可以關(guān)閉</p>
? ? ? ? </div>
? ? </div>
</div>
var app = new Vue({
? ? el:'#app',
? ? data:{
? ? ? ? show:false
? ? },
? ? methods:{
? ? handleClose:{
? ? ? ? this.show=false;
? ? ? ? }
? ? }
});
? ? ? ? 邏輯很簡單,點擊按鈕時顯示class為dropdown的div元素帅戒。
? ? ? ? 自定義指令v-clickoutside綁定了一個函數(shù)handleClose,原來關(guān)閉菜單崖技。先來看一下clickoutside.js中的內(nèi)容:
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鉤子內(nèi)聲明了一個函數(shù)documentHandler迎献,并將它作為句柄綁定在document的click事件上瞎访。documentHandler函數(shù)做了兩個判斷,第一個是判斷點擊的區(qū)域是否是指令所在的元素內(nèi)部吁恍,如果是扒秸,就跳出函數(shù),不往下繼續(xù)執(zhí)行冀瓦。
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>
? ? ? ? 第二個判斷的是當(dāng)前的指令v-clickoutside有沒有寫表達式拾徙,在該自定義指令中,表達式應(yīng)該是一個函數(shù)感局,在過濾了內(nèi)部元素后尼啡,點擊外面任何區(qū)域應(yīng)該執(zhí)行用戶表達式中的函數(shù),所以binding.value()就是用來執(zhí)行當(dāng)前上下文methods中指定的函數(shù)的询微。
? ? ? ? 與Vue1.x不同的是崖瞭,在自定義指令中,不能再用this.xxx的形式在上下文中聲明一個變量撑毛。所以用el._vueClickOutside_引用了doucumentHandler书聚,這樣就可以在unbind鉤子里移除對document的click事件監(jiān)聽。如果不移除,當(dāng)組件或元素銷毀時寺惫,它仍然存在于內(nèi)存中疹吃。
? ? ? ? 以上代碼分解完整代碼基本一致,不再重復(fù)提供西雀。下面是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開發(fā)一個實時事件轉(zhuǎn)換指令v-time
? ? ? ? 在一些社區(qū)萨驶,比如微博、朋友圈等艇肴,發(fā)布的動態(tài)會有一個相對本機時間轉(zhuǎn)換后的相對時間腔呜。(2小時前,11天前等).
? ? ? ? 一般在服務(wù)器的存儲事件格式是Unix時間戳再悼,比如2017-01-01 00:00:00的時間戳是1483200000.前端在拿到數(shù)據(jù)后核畴,將它轉(zhuǎn)換為可讀的時間格式再顯示出來。為了顯出實時性冲九,在一些社交類產(chǎn)品中谤草,甚至?xí)崟r轉(zhuǎn)換為幾秒鐘前、幾分鐘前莺奸、幾小時前等不同的格式丑孩,這樣比直接轉(zhuǎn)換為年、月灭贷、日温学、時、分甚疟、秒更友好仗岖。本示例就來實現(xiàn)這樣一個自定義指令v-time,將表達式傳入的時間戳實時轉(zhuǎn)換為相對時間览妖。
? ? ? ? 便于演示效果轧拄,我們初始化時定義了兩個時間。
index.html
<!DOCTYPE html>
<html>
<head>
? ? <meta charset="utf-8">
? ? <title>時間轉(zhuǎn)換指令</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:本示例所用的時間戳都是毫秒級紧帕,如服務(wù)端返回秒級時間戳需要乘以1000后再使用。
? ? ? ? 分析一下時間轉(zhuǎn)換邏輯:
? ? ? ? 1分鐘以前桅打,顯示“剛剛”
? ? ? ? 1分鐘-1小時之間是嗜,顯示“xx分鐘前”。
? ? ? ? 1小時-24小時之間挺尾,顯示:"xx小時前“鹅搪。
? ? ? ? 1天-一個月(31天)間,顯示:"xx天前”遭铺。
? ? ? ? 大于1個月丽柿,顯示“xx年xx月xx日”恢准。
? ? ? ? 為了使判斷邏輯更簡單,統(tǒng)一使用時間戳進行時間大小判斷甫题。在寫指令v-time之前馁筐,需要先寫一系列與時間相關(guān)的函數(shù),我們聲明一個對象Time坠非,把它們都封裝在里面敏沉。
time.js
? ? var time = {//獲取當(dāng)前時間戳
? ? ? ? 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;
? ? ? ? },
? ? //轉(zhuǎn)換時間
? ? getFormatTime:function(){
? ? ? ? var now = this.getUnix();//當(dāng)前時間戳
? ? ? ? var today=this.getTodayUnix();//今天0點時間戳
? ? ? ? var year = this.getYearUnix();//今年0點時間戳
? ? ? ? var timer = (now -timestamp)/1000;//轉(zhuǎn)換為秒級時間戳
? ? ? ? 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所需要的潦闲,入?yún)楹撩爰墪r間戳攒菠,返回已經(jīng)整理號事件格式的字符串。
? ? ? ? 最后在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作為參數(shù)傳入Time.getFormatTime()方法得到格式化時間辖众,再通過el/innerHTML寫入指令坐在元素。定時器el._timeout_每分鐘出發(fā)一次和敬,更新時間赵辕,并且在unbind鉤子里清除掉。
? ? ? ? 總結(jié):在編寫自定義指令時概龄,給DOM綁定一次性事件等初始條件,建議在bind鉤子內(nèi)完成饲握。同時要在unbind鉤子內(nèi)解除相關(guān)綁定私杜。在自定義指令里,理論上可以任意操作DOM救欧,但這又違背了Vue.js的初衷衰粹,所以對于大幅度 DOM變動,應(yīng)該使用組件笆怠。
下一章:Render函數(shù)(進階篇)-未更新