vue自定義指令--directive

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。

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

項目中的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,咱們應該如何去解決呢?

一、事件解綁,從新綁定

咱們在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

能夠看到,數據已經變成咱們想要的數據了。

二、把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>

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

相關文章
相關標籤/搜索