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