? ? ? ? 在第五章中我們已經介紹了需要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變動,應該使用組件。
下一章:Render函數(進階篇)-未更新