Vue中內置了不少的指令,如v-model、v-show、v-html等,可是有時候這些指令並不能知足咱們,或者說咱們想爲元素附加一些特別的功能,這時候,咱們就須要用到vue中一個很強大的功能了—自定義指令。html
在開始以前,咱們須要明確一點,自定義指令解決的問題或者說使用場景是對普通 DOM 元素進行底層操做,因此咱們不能盲目的胡亂的使用自定義指令。vue
就像vue中有全局組件和局部組件同樣,他也分全局自定義指令和局部指令。node
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改成一個函數。app
let Opt = function(el,binding,vnode){ }
對於自定義指令的使用是很是簡單的,若是你對vue有必定了解的話。dom
咱們能夠像v-text=」’test’」
同樣,把咱們須要傳遞的值放在‘=’號後面傳遞過去。ide
咱們能夠像v-on:click=」handClick」
同樣,爲指令傳遞參數’click’。函數
咱們能夠像v-on:click.stop=」handClick」
同樣,爲指令添加一個修飾符。源碼分析
咱們也能夠像v-once
同樣,什麼都不傳遞。
每一個指令,他的底層封裝確定都不同,因此咱們應該先了解他的功能和用法,再去使用它。
上面咱們也介紹了,自定義指令一共有5個鉤子函數,他們分別是:bind、inserted、update、componentUpdate和unbind。
對於這幾個鉤子函數,瞭解的能夠自行跳過,不瞭解的我也不介紹,本身去官網看,沒有比官網上說的更詳細的了:鉤子函數
在項目中,咱們自定義一個全局指令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>
能夠看到,當咱們點擊元素的時候,成功打印了元素,以及傳遞過去的數據。
但是,當咱們把最後一個元素動態的改成8以後(6 --> 8),點擊元素,元素是對的,但是打印的數據卻仍然是6.
或者,當咱們刪除了第一個元素以後,點擊元素
這是爲何呢????帶着這個疑問,我去看了看源碼。在進行下面的源碼分析以前,先來講結論:
組件進行初始化的時候,也就是第一次運行指令的時候,會執行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函數。
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,咱們應該如何去解決呢?
一、事件解綁,從新綁定
咱們在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) }) })
能夠看到,數據已經變成咱們想要的數據了。
二、把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 } })
這樣也能達到咱們想要的效果。
三、更新父元素
若是咱們爲父元素ul綁定一個變化的key值,這樣,當數據變動的時候就會更新父元素,從而從新建立子元素,達到從新綁定指令的效果。
<ul :key="Date.now()"> <li v-for="(item,index) in arr" :key="index" v-my-click="item">{{item}}</li> </ul>
這樣也能達到咱們想要的效果。