vue自定義指令--directive

Vue中內置了很多的指令,如v-model、v-show、v-html等,但是有時候這些指令並不能滿足我們,或者說我們想爲元素附加一些特別的功能,這時候,我們就需要用到vue中一個很強大的功能了—自定義指令。

在開始之前,我們需要明確一點,自定義指令解決的問題或者說使用場景是對普通 DOM 元素進行底層操作,所以我們不能盲目的胡亂的使用自定義指令。

如何聲明自定義指令?

就像vue中有全局組件和局部組件一樣,他也分全局自定義指令和局部指令。

let Opt = {
	bind:function(el,binding,vnode){ },
	inserted:function(el,binding,vnode){ },
	update:function(el,binding,vnode){ },
	componentUpdated:function(el,binding,vnode){ },
	unbind:function(el,binding,vnode){ },
}

對於全局自定義指令的創建,我們需要使用Vue.directive接口

Vue.directive('demo', Opt)

對於局部組件,我們需要在組件的鉤子函數directives中進行聲明

Directives: {
	Demo: 	Opt
}

Vue中的指令可以簡寫,上面Opt是一個對象,包含了5個鉤子函數,我們可以根據需要只寫其中幾個函數。如果你想在 bind 和 update 時觸發相同行爲,而不關心其它的鉤子,那麼你可以將Opt改爲一個函數。

let Opt = function(el,binding,vnode){ }

如何使用自定義指令?

對於自定義指令的使用是非常簡單的,如果你對vue有一定了解的話。

我們可以像v-text=”’test’”一樣,把我們需要傳遞的值放在‘=’號後面傳遞過去。

我們可以像v-on:click=”handClick”一樣,爲指令傳遞參數’click’。

我們可以像v-on:click.stop=”handClick”一樣,爲指令添加一個修飾符。

我們也可以像v-once一樣,什麼都不傳遞。

每個指令,他的底層封裝肯定都不一樣,所以我們應該先了解他的功能和用法,再去使用它。

自定義指令的 鉤子函數

上面我們也介紹了,自定義指令一共有5個鉤子函數,他們分別是:bind、inserted、update、componentUpdate和unbind。

對於這幾個鉤子函數,瞭解的可以自行跳過,不瞭解的我也不介紹,自己去官網看,沒有比官網上說的更詳細的了:鉤子函數

項目中的bug

在項目中,我們自定義一個全局指令my-click

Vue.directive('my-click',{
	bind:function(el, binding, vnode, oldVnode){
		el.addEventListener('click',function(){
			console.log(el, binding.value)
		})
	}
})

同時,有一個數組arr:[1,2,3,4,5,6],我們遍歷數組,生成dom元素,併爲元素綁定指令:

<ul>
	<li v-for="(item,index) in arr" :key="index" v-my-click="item">{{item}}</li>
</ul>

click

可以看到,當我們點擊元素的時候,成功打印了元素,以及傳遞過去的數據。

可是,當我們把最後一個元素動態的改爲8之後(6 --> 8),點擊元素,元素是對的,可是打印的數據卻仍然是6.

click

或者,當我們刪除了第一個元素之後,點擊元素

click

黑人問號臉,這是爲什麼呢????帶着這個疑問,我去看了看源碼。在進行下面的源碼分析之前,先來說結論:

組件進行初始化的時候,也就是第一次運行指令的時候,會執行bind鉤子函數,我們所傳入的參數(binding)都進入到了這裏,並形成了一個閉包。

當我們進行數據更新的時候,vue虛擬dom不會銷燬這個組件(如果說刪除某個數據,會從後往前銷燬組件,前面的總是最後銷燬),而是進行更新(根據數據改變),如果指令有update鉤子會運行這個鉤子函數,但是對於元素在bind中綁定的事件,在update中沒有處理的話,他不會消失(依然引用初始化時形成的閉包中的數據),所以當我們更改數據再次點擊元素後,看到的數據還是原數據。

源碼分析

函數執行順序:createElm/initComponent/patchVnode --> invokeCreateHooks (cbs.create) --> updateDirectives --> _update

在createElm方法和initComponent方法和更新節點patchVnode時會調用invokeCreateHooks方法,它會去遍歷cbs.create中鉤子函數進行執行,cbs.create中的鉤子函數如下圖所示共8個。我們所需要看的就是updateDirectives這個函數,這個函數會繼續調用_update函數,vue中的指令操作就都在這個_update函數中了。

_update

下面我們就來詳細看下這個_update函數。

function _update(oldVnode, vnode) {
	//判斷舊節點是不是空節點,是的話表示新建/初始化組件
	var isCreate = oldVnode === emptyNode;
	//判斷新節點是不是空節點,是的話表示銷燬組件
	var isDestroy = vnode === emptyNode;
	//獲取舊節點上的所有自定義指令
	var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);
	//獲取新節點上的所有自定義指令
	var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);

	//保存inserted鉤子函數
	var dirsWithInsert = [];
	//保存componentUpdated鉤子函數
	var dirsWithPostpatch = [];

	var key, oldDir, dir;
	
	//這裏先說下callHook$1函數的作用
	//callHook$1有五個參數,第一個參數是指令對象,第二個參數是鉤子函數名稱,第三個參數新節點,
	//第四個參數是舊節點,第五個參數是是否爲註銷組件,默認爲undefined,只在組件註銷時使用
	//在這個函數裏,會根據我們傳遞的鉤子函數名稱,運行我們自定義組件時,所聲明的鉤子函數,
	
	//遍歷所有新節點上的自定義指令
	for(key in newDirs) {
		oldDir = oldDirs[key];
		dir = newDirs[key];
		//如果舊節點中沒有對應的指令,一般都是初始化的時候運行
		if(!oldDir) {
			//對該節點執行指令的bind鉤子函數
			callHook$1(dir, 'bind', vnode, oldVnode);
			//dir.def是我們所定義的指令的五個鉤子函數的集合
			//如果我們的指令中存在inserted鉤子函數
			if(dir.def && dir.def.inserted) {
				//把該指令存入dirsWithInsert中
				dirsWithInsert.push(dir);
			}
		} else { 
			//如果舊節點中有對應的指令,一般都是組件更新的時候運行
			//那麼這裏進行更新操作,運行update鉤子(如果有的話)
			//將舊值保存下來,供其他地方使用(僅在 update 和 componentUpdated 鉤子中可用)
			dir.oldValue = oldDir.value;
			//對該節點執行指令的update鉤子函數
			callHook$1(dir, 'update', vnode, oldVnode);
			//dir.def是我們所定義的指令的五個鉤子函數的集合
			//如果我們的指令中存在componentUpdated鉤子函數
			if(dir.def && dir.def.componentUpdated) {
				//把該指令存入dirsWithPostpatch中
				dirsWithPostpatch.push(dir);
			}
		}
	}
	
	//我們先來簡單講下mergeVNodeHook的作用
	//mergeVNodeHook有三個參數,第一個參數是vnode節點,第二個參數是key值,第三個參數是回函數
	//mergeVNodeHook會先用一個函數wrappedHook重新封裝回調,在這個函數裏運行回調函數
	//如果該節點沒有這個key屬性,會新增一個key屬性,值爲一個數組,數組中包含上面說的函數wrappedHook
	//如果該節點有這個key屬性,會把函數wrappedHook追加到數組中
	
	//如果dirsWithInsert的長度不爲0,也就是在初始化的時候,且至少有一個指令中有inserted鉤子函數
	if(dirsWithInsert.length) {
		//封裝回調函數
		var callInsert = function() {
			//遍歷所有指令的inserted鉤子
			for(var i = 0; i < dirsWithInsert.length; i++) {
				//對節點執行指令的inserted鉤子函數
				callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);
			}
		};
		if(isCreate) {
			//如果是新建/初始化組件,使用mergeVNodeHook綁定insert屬性,等待後面調用。
			mergeVNodeHook(vnode, 'insert', callInsert);
		} else {
			//如果是更新組件,直接調用函數,遍歷inserted鉤子
			callInsert();
		}
	}
	
	//如果dirsWithPostpatch的長度不爲0,也就是在組件更新的時候,且至少有一個指令中有componentUpdated鉤子函數
	if(dirsWithPostpatch.length) {
		//使用mergeVNodeHook綁定postpatch屬性,等待後面子組建全部更新完成調用。
		mergeVNodeHook(vnode, 'postpatch', function() {
			for(var i = 0; i < dirsWithPostpatch.length; i++) {
				//對節點執行指令的componentUpdated鉤子函數
				callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
			}
		});
	}
	
	//如果不是新建/初始化組件,也就是說是更新組件
	if(!isCreate) {
		//遍歷舊節點中的指令
		for(key in oldDirs) {
			//如果新節點中沒有這個指令(舊節點中有,新節點沒有)
			if(!newDirs[key]) {
				//從舊節點中解綁,isDestroy表示組件是不是註銷了
				//對舊節點執行指令的unbind鉤子函數
				callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
			}
		}
	}
}

callHook$1函數

function callHook$1(dir, hook, vnode, oldVnode, isDestroy) {
	var fn = dir.def && dir.def[hook];
	if(fn) {
		try {
			fn(vnode.elm, dir, vnode, oldVnode, isDestroy);
		} catch(e) {
			handleError(e, vnode.context, ("directive " + (dir.name) + " " + hook + " hook"));
		}
	}
}

解決

看過了源碼,我們再回到上面的bug,我們應該如何去解決呢?

1、事件解綁,重新綁定

我們在bind鉤子中綁定了事件,當數據更新後,會運行update鉤子,所以我們可以在update中先解綁再重新進行綁定。因爲bind和update中的內容差不多,所以我們可以把bind和update合併爲同一個函數,在用自定義指令的簡寫方法寫成下面的代碼:

Vue.directive('my-click', function(el, binding, vnode, oldVnode){
	//點擊事件的回調掛在在元素myClick屬性上
	el.myClick && el.removeEventListener('click', el.myClick);
	el.addEventListener('click', el.myClick = function(){
		console.log(el, binding.value)
	})
})

click

可以看到,數據已經變成我們想要的數據了。

2、把binding掛在到元素上,更新數據後更新binding

我們已經知道了,造成問題的根本原因是初始化運行bind鉤子的時候爲元素綁定事件,事件內獲取的數據是初始化的時候傳遞過來的數據,因爲形成了閉包,那麼我們不使用能引起閉包的數據,把數據存到某一個地方,然後去更新這個數據。

Vue.directive('my-click',{
	bind: function(el, binding, vnode, oldVnode){
		el.binding = binding
		el.addEventListener('click', function(){
			var binding = this.binding
			console.log(this, binding.value)
		})
	},
	update: function(el, binding, vnode, oldVnode){
		el.binding = binding
	}
})

這樣也能達到我們想要的效果。

3、更新父元素

如果我們爲父元素ul綁定一個變化的key值,這樣,當數據變更的時候就會更新父元素,從而重新創建子元素,達到重新綁定指令的效果。

<ul :key="Date.now()">
	<li v-for="(item,index) in arr" :key="index" v-my-click="item">{{item}}</li>
</ul>

這樣也能達到我們想要的效果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章