第八章 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閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容