vue自定義指令directive的使用方法
Vue中內(nèi)置了很多的指令,如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){ }, }
對于全局自定義指令的創(chuàng)建,我們需要使用 Vue.directive
接口
Vue.directive('demo', Opt)
對于局部組件,我們需要在組件的鉤子函數(shù)directives中進行聲明
Directives: { Demo: Opt }
Vue中的指令可以簡寫,上面Opt是一個對象,包含了5個鉤子函數(shù),我們可以根據(jù)需要只寫其中幾個函數(shù)。如果你想在 bind 和 update 時觸發(fā)相同行為,而不關(guān)心其它的鉤子,那么你可以將Opt改為一個函數(shù)。
let Opt = function(el,binding,vnode){ }
如何使用自定義指令?
對于自定義指令的使用是非常簡單的,如果你對vue有一定了解的話。
我們可以像v-text=”'test'”
一樣,把我們需要傳遞的值放在‘='號后面?zhèn)鬟f過去。
我們可以像v-on:click=”handClick”
一樣,為指令傳遞參數(shù)'click'。
我們可以像v-on:click.stop=”handClick”
一樣,為指令添加一個修飾符。
我們也可以像v-once
一樣,什么都不傳遞。
每個指令,他的底層封裝肯定都不一樣,所以我們應該先了解他的功能和用法,再去使用它。
自定義指令的 鉤子函數(shù)
上面我們也介紹了,自定義指令一共有5個鉤子函數(shù),他們分別是:bind、inserted、update、componentUpdate和unbind。
一個指令定義對象可以提供如下幾個鉤子函數(shù) (均為可選):
- bind:只調(diào)用一次,指令第一次綁定到元素時調(diào)用。在這里可以進行一次性的初始化設置。
- inserted:被綁定元素插入父節(jié)點時調(diào)用 (僅保證父節(jié)點存在,但不一定已被插入文檔中)。
- update:所在組件的 VNode 更新時調(diào)用,但是可能發(fā)生在其子 VNode 更新之前。指令的值可能發(fā)生了改變,也可能沒有。但是你可以通過比較更新前后的值來忽略不必要的模板更新 (詳細的鉤子函數(shù)參數(shù)見下)。
- componentUpdated:指令所在組件的 VNode 及其子 VNode 全部更新后調(diào)用。
- unbind:只調(diào)用一次,指令與元素解綁時調(diào)用。
指令鉤子函數(shù)會被傳入以下參數(shù):
- el:指令所綁定的元素,可以用來直接操作 DOM 。
- binding:一個對象,包含以下屬性:
- name:指令名,不包括 v- 前綴。
- value:指令的綁定值,例如:v-my-directive="1 + 1" 中,綁定值為 2。
- oldValue:指令綁定的前一個值,僅在 update 和 componentUpdated 鉤子中可用。無論值是否改變都可用。
- expression:字符串形式的指令表達式。例如 v-my-directive="1 + 1" 中,表達式為 "1 + 1"。
- arg:傳給指令的參數(shù),可選。例如 v-my-directive:foo 中,參數(shù)為 "foo"。
- modifiers:一個包含修飾符的對象。例如:v-my-directive.foo.bar 中,修飾符對象為 { foo: true, bar: true }。
- vnode:Vue 編譯生成的虛擬節(jié)點。移步 VNode API 來了解更多詳情。
- oldVnode:上一個虛擬節(jié)點,僅在 update 和 componentUpdated 鉤子中可用。
對于這幾個鉤子函數(shù),了解的可以自行跳過,不了解的我也不介紹,自己去官網(wǎng)看,沒有比官網(wǎng)上說的更詳細的了:鉤子函數(shù)
項目中的bug
在項目中,我們自定義一個全局指令my-click
:
Vue.directive('my-click',{ bind:function(el, binding, vnode, oldVnode){ el.addEventListener('click',function(){ console.log(el, binding.value) }) } })
同時,有一個數(shù)組arr:[1,2,3,4,5,6]
,我們遍歷數(shù)組,生成dom元素,并為元素綁定指令:
<ul> <li v-for="(item,index) in arr" :key="index" v-my-click="item">{{item}}</li> </ul>
可以看到,當我們點擊元素的時候,成功打印了元素,以及傳遞過去的數(shù)據(jù)。
可是,當我們把最后一個元素動態(tài)的改為8之后(6 --> 8),點擊元素,元素是對的,可是打印的數(shù)據(jù)卻仍然是6.
或者,當我們刪除了第一個元素之后,點擊元素
黑人問號臉,這是為什么呢????帶著這個疑問,我去看了看源碼。在進行下面的源碼分析之前,先來說結(jié)論:
組件進行初始化的時候,也就是第一次運行指令的時候,會執(zhí)行bind鉤子函數(shù),我們所傳入的參數(shù)(binding)都進入到了這里,并形成了一個閉包。
當我們進行數(shù)據(jù)更新的時候,vue虛擬dom不會銷毀這個組件(如果說刪除某個數(shù)據(jù),會從后往前銷毀組件,前面的總是最后銷毀),而是進行更新(根據(jù)數(shù)據(jù)改變),如果指令有update鉤子會運行這個鉤子函數(shù),但是對于元素在bind中綁定的事件,在update中沒有處理的話,他不會消失(依然引用初始化時形成的閉包中的數(shù)據(jù)),所以當我們更改數(shù)據(jù)再次點擊元素后,看到的數(shù)據(jù)還是原數(shù)據(jù)。
源碼分析
函數(shù)執(zhí)行順序:createElm/initComponent/patchVnode --> invokeCreateHooks (cbs.create) --> updateDirectives --> _update
在createElm方法和initComponent方法和更新節(jié)點patchVnode時會調(diào)用invokeCreateHooks方法,它會去遍歷cbs.create中鉤子函數(shù)進行執(zhí)行,cbs.create中的鉤子函數(shù)如下圖所示共8個。我們所需要看的就是updateDirectives這個函數(shù),這個函數(shù)會繼續(xù)調(diào)用_update函數(shù),vue中的指令操作就都在這個_update函數(shù)中了。
下面我們就來詳細看下這個_update函數(shù)。
function _update(oldVnode, vnode) { //判斷舊節(jié)點是不是空節(jié)點,是的話表示新建/初始化組件 var isCreate = oldVnode === emptyNode; //判斷新節(jié)點是不是空節(jié)點,是的話表示銷毀組件 var isDestroy = vnode === emptyNode; //獲取舊節(jié)點上的所有自定義指令 var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context); //獲取新節(jié)點上的所有自定義指令 var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context); //保存inserted鉤子函數(shù) var dirsWithInsert = []; //保存componentUpdated鉤子函數(shù) var dirsWithPostpatch = []; var key, oldDir, dir; //這里先說下callHook$1函數(shù)的作用 //callHook$1有五個參數(shù),第一個參數(shù)是指令對象,第二個參數(shù)是鉤子函數(shù)名稱,第三個參數(shù)新節(jié)點, //第四個參數(shù)是舊節(jié)點,第五個參數(shù)是是否為注銷組件,默認為undefined,只在組件注銷時使用 //在這個函數(shù)里,會根據(jù)我們傳遞的鉤子函數(shù)名稱,運行我們自定義組件時,所聲明的鉤子函數(shù), //遍歷所有新節(jié)點上的自定義指令 for(key in newDirs) { oldDir = oldDirs[key]; dir = newDirs[key]; //如果舊節(jié)點中沒有對應的指令,一般都是初始化的時候運行 if(!oldDir) { //對該節(jié)點執(zhí)行指令的bind鉤子函數(shù) callHook$1(dir, 'bind', vnode, oldVnode); //dir.def是我們所定義的指令的五個鉤子函數(shù)的集合 //如果我們的指令中存在inserted鉤子函數(shù) if(dir.def && dir.def.inserted) { //把該指令存入dirsWithInsert中 dirsWithInsert.push(dir); } } else { //如果舊節(jié)點中有對應的指令,一般都是組件更新的時候運行 //那么這里進行更新操作,運行update鉤子(如果有的話) //將舊值保存下來,供其他地方使用(僅在 update 和 componentUpdated 鉤子中可用) dir.oldValue = oldDir.value; //對該節(jié)點執(zhí)行指令的update鉤子函數(shù) callHook$1(dir, 'update', vnode, oldVnode); //dir.def是我們所定義的指令的五個鉤子函數(shù)的集合 //如果我們的指令中存在componentUpdated鉤子函數(shù) if(dir.def && dir.def.componentUpdated) { //把該指令存入dirsWithPostpatch中 dirsWithPostpatch.push(dir); } } } //我們先來簡單講下mergeVNodeHook的作用 //mergeVNodeHook有三個參數(shù),第一個參數(shù)是vnode節(jié)點,第二個參數(shù)是key值,第三個參數(shù)是回函數(shù) //mergeVNodeHook會先用一個函數(shù)wrappedHook重新封裝回調(diào),在這個函數(shù)里運行回調(diào)函數(shù) //如果該節(jié)點沒有這個key屬性,會新增一個key屬性,值為一個數(shù)組,數(shù)組中包含上面說的函數(shù)wrappedHook //如果該節(jié)點有這個key屬性,會把函數(shù)wrappedHook追加到數(shù)組中 //如果dirsWithInsert的長度不為0,也就是在初始化的時候,且至少有一個指令中有inserted鉤子函數(shù) if(dirsWithInsert.length) { //封裝回調(diào)函數(shù) var callInsert = function() { //遍歷所有指令的inserted鉤子 for(var i = 0; i < dirsWithInsert.length; i++) { //對節(jié)點執(zhí)行指令的inserted鉤子函數(shù) callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode); } }; if(isCreate) { //如果是新建/初始化組件,使用mergeVNodeHook綁定insert屬性,等待后面調(diào)用。 mergeVNodeHook(vnode, 'insert', callInsert); } else { //如果是更新組件,直接調(diào)用函數(shù),遍歷inserted鉤子 callInsert(); } } //如果dirsWithPostpatch的長度不為0,也就是在組件更新的時候,且至少有一個指令中有componentUpdated鉤子函數(shù) if(dirsWithPostpatch.length) { //使用mergeVNodeHook綁定postpatch屬性,等待后面子組建全部更新完成調(diào)用。 mergeVNodeHook(vnode, 'postpatch', function() { for(var i = 0; i < dirsWithPostpatch.length; i++) { //對節(jié)點執(zhí)行指令的componentUpdated鉤子函數(shù) callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode); } }); } //如果不是新建/初始化組件,也就是說是更新組件 if(!isCreate) { //遍歷舊節(jié)點中的指令 for(key in oldDirs) { //如果新節(jié)點中沒有這個指令(舊節(jié)點中有,新節(jié)點沒有) if(!newDirs[key]) { //從舊節(jié)點中解綁,isDestroy表示組件是不是注銷了 //對舊節(jié)點執(zhí)行指令的unbind鉤子函數(shù) callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy); } } } }
callHook$1函數(shù)
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鉤子中綁定了事件,當數(shù)據(jù)更新后,會運行update鉤子,所以我們可以在update中先解綁再重新進行綁定。因為bind和update中的內(nèi)容差不多,所以我們可以把bind和update合并為同一個函數(shù),在用自定義指令的簡寫方法寫成下面的代碼:
Vue.directive('my-click', function(el, binding, vnode, oldVnode){ //點擊事件的回調(diào)掛在在元素myClick屬性上 el.myClick && el.removeEventListener('click', el.myClick); el.addEventListener('click', el.myClick = function(){ console.log(el, binding.value) }) })
可以看到,數(shù)據(jù)已經(jīng)變成我們想要的數(shù)據(jù)了。
2、把binding掛在到元素上,更新數(shù)據(jù)后更新binding
我們已經(jīng)知道了,造成問題的根本原因是初始化運行bind鉤子的時候為元素綁定事件,事件內(nèi)獲取的數(shù)據(jù)是初始化的時候傳遞過來的數(shù)據(jù),因為形成了閉包,那么我們不使用能引起閉包的數(shù)據(jù),把數(shù)據(jù)存到某一個地方,然后去更新這個數(shù)據(jù)。
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、更新父元素
如果我們?yōu)楦冈豼l綁定一個變化的key值,這樣,當數(shù)據(jù)變更的時候就會更新父元素,從而重新創(chuàng)建子元素,達到重新綁定指令的效果。
<ul :key="Date.now()"> <li v-for="(item,index) in arr" :key="index" v-my-click="item">{{item}}</li> </ul>
這樣也能達到我們想要的效果。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Vue2.5 結(jié)合 Element UI 之 Table 和 Pagination 組件實現(xiàn)分頁功能
這篇文章主要介紹了Vue2.5 結(jié)合 Element UI 之 Table 和 Pagination 組件實現(xiàn)分頁功能,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2018-01-01vue刷新子組件、重置組件以及重新加載子組件項目實戰(zhàn)記錄
在vue開發(fā)中出于各種目的,我們常常需要讓組件重新加載渲染,這篇文章主要給大家介紹了關(guān)于vue刷新子組件、重置組件以及重新加載子組件的相關(guān)資料,需要的朋友可以參考下2023-12-12elementplus?card?懸浮菜單的實現(xiàn)
本文主要介紹了elementplus?card?懸浮菜單的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-07-07vue2模擬vue-element-admin手寫角色權(quán)限的實現(xiàn)
本文主要介紹了vue2模擬vue-element-admin手寫角色權(quán)限的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-07-07Element控件Tree實現(xiàn)數(shù)據(jù)樹形結(jié)構(gòu)的示例代碼
我們在開發(fā)中肯定會遇到用樹形展示數(shù)據(jù)的需求,本文主要介紹了Element控件Tree實現(xiàn)數(shù)據(jù)樹形結(jié)構(gòu)的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-08-08Vue2.5通過json文件讀取數(shù)據(jù)的方法
本文通過實例代碼給大家詳細介紹了Vue2.5通過json文件讀取數(shù)據(jù)的方法,非常不錯,具有參考借鑒價值,需要的朋友參考下吧2018-02-02vue點擊彈窗自動觸發(fā)點擊事件的解決辦法(模擬場景)
本文通過案例場景給大家介紹vue點擊彈窗自動觸發(fā)點擊事件的解決辦法,通過兩種方法給大家分享vue 自動觸發(fā)點擊事件的處理方法,對vue自動觸發(fā)點擊事件相關(guān)知識感興趣的朋友一起看看吧2021-05-05