Vue 自定義指令
請勿直接爬走,本文地址:https://www.cnblogs.com/xiaoxuStudy/p/13208406.html
目錄:
1. 怎麼建立自定義指令
2. 什麼時候用自定義指令
3. 鉤子函式
4. 實操
通過 Vue.directive 全域性建立指令,Vue.directive 的第一個引數定義了指令的名稱,如下程式碼建立了一個名為 resize 的指令。
Vue.directive("resize", {
});
在全域性註冊這個指令之後,意味著可以任意元件中使用這個指令,可以直接在單檔案元件的模板中直接使用指令,也可以在 JSX 中使用指令。按照約定,指令名字有 “v-” 字首,字首用於標明這是一個字首。
關於什麼時候用自定義指令,其邏輯與使用事件修飾符的邏輯是一樣的。
使用事件修飾符很大程度上是為了讓我們的程式碼顯得是資料驅動並且易於測試的,將 DOM 的邏輯單獨委託出來,約定成一些特定的修飾符。(事件修飾符相關筆記:https://www.cnblogs.com/xiaoxuStudy/p/13233379.html#oneone)
其實,自定義指令也是一樣的邏輯,當我們的 methods 中存在操作 DOM/BOM 相關的邏輯的時候,就該思考是否可以將其抽象成一個自定義指令,以便於業務邏輯與相關 DOM 操作解耦,並且使之更容易被單元測試。
Vue 在這裡嚴格遵循了設計模式中的開閉原則,通過約定的鉤子函式來讓開發者可以在不同的時機中去操作元件。(Vue官網鉤子函式相關:https://cn.vuejs.org/v2/guide/custom-directive.html#%E9%92%A9%E5%AD%90%E5%87%BD%E6%95%B0)
1. 鉤子函式
Vue.directive("resize", { //只調用一次,指令第一次繫結元素時呼叫 //在這裡可以進行一次性的初始化設定 bind: function(el, binding, value){}, //被繫結元素插入父節點時呼叫 //(僅保證父節點存在,但不一定已被插入文件中) inserted: function(el, binding, vnode){}, //所在元件的 Vnode 更新時呼叫 //但是可能發生在其子 VNode 更新之前 //指令的值可能發生了變化,也可能沒有 //但是可以通過比較更新前後的值來忽略不必要的模板更新 update: function(el, binding, vnode, oldVnode){}, //指令所在的 VNode 及其子 VNode 全部更新後呼叫 componentUpdated: function(el, binding, vnode, oldVnode){}, //只調用一次,指令與元素解綁時呼叫 unbind: function(el, binding, vnode){}, });鉤子函式例子
先來看第一對鉤子函式 bind 與 unbind 函式,顧名思義,這兩個鉤子函式是在當前這個指令宣告的元素繫結和解綁時被呼叫的,並且需要記住的是,bind 與 unbind 都只會被呼叫一次。
接下來看鉤子函式 inserted。通常情況下,inserted 會在 bind 之後被呼叫。
bind 跟 inserted 的區別是:bind 中引數 el.parentNode 為 null,inserted 中可以通過 el.parentNode 訪問當前節點的父節點。當有資訊需要存放在父節點上、需要訪問父節點時,使用 inserted 的頻率高於 bind 。
接下來看最後一組鉤子函式 update 跟 componentUpdate,這對鉤子函式會在 vnode 更新前後被呼叫。
與其他鉤子函式相比,update 跟 componentUpdate 傳入的引數多一個 oldVnode,oldVnode 代表之前的 Virtual DOM 節點資訊,vnode代表當前的Virtual DOM 節點資訊。可以根據比較 oldVnode 和 vnode 之間的差異來判斷模板是否需要更新,以減少不必要的模板更新,從而一定程度提高元件效能。
2. 鉤子函式引數
function(
// 指令所繫結的元素,可以用來直接操作 DOM
el,
// binding 一個物件,包含以下屬性
{
// 指令名,不包括 -v 字首
name,
// 指令的繫結值,例如:v-my-directive="1+1"中,繫結值為 2
value,
// 指令繫結的前一個值
// 僅在 update 和 componentUpdated 鉤子中可用
oldValue,
//字串形式的指令表示式
//例如 v-my-directive="1+1" 中,表示式為 "1+1"
expression,
//例如指令的引數,可選。
//例如 v-my-directive:foo 中,引數為 "foo"
arg,
//一個包含修飾符的物件
//例如:v-my-directive.foo.bar 中,
//修飾符物件為 {foo: true, bar: true}
modifiers
},
//Vue 編譯生成的虛擬節點
vnode,
//上一個虛擬節點,僅在 update 和 componentUpdated 鉤子中可用
oldVnode
)
鉤子函式引數
除了 el 之後,其它引數都應該是隻讀的,切勿進行修改。如果需要在鉤子之間共享資料,建議通過元素的 dataset 來進行。
需求
需求1. 完成一個 v-resize 指令,去監聽瀏覽器大小的改變,改變的時候通過監聽 onResize 響應。然後,將高度或寬度列印到頁面上。
需求2. 自定義一個引數 direction, 控制監聽頁面高度或者寬度的變化。
需求3. 新增完成一個修飾符 .quiet,來控制是否在指令初始化的時候響應 onResize 函式。
實現
本例主要編寫三個檔案,分別是 main.js、App.vue、SDirectivePage.vue,main.js 是入口檔案,index.html 是一級頁面,App.vue 是二級頁面,SDirectivePage.vue 是 App.vue 的子元件。
需求1
在 main.js 註冊全域性自定義指令 v-resize, 寫鉤子函式的時候要選擇寫 bind 或者 inserted,bind 跟 inserted 的區別是 inserted 能訪問父節點 bind 不能,因為不確定需不需訪問父節點,先選用 inserted。在 inserted 裡面寫函式,當頁面載入時,執行該函式。給該函式傳入引數 el 跟 binding,將回調函式從 binding 中取出來,binding.value 對應在 SDirectivePage.vue 的模板裡使用自定義指令 v-resize 時定義指令的表示式 onResize,onResize 是一個方法,通過 binding 取出回撥函式後將回調函式 onResize 賦值給 callback 變數,然後使用 addEventListener 在 window 上新增 resize 的事件處理程式,用於監聽瀏覽器的 onResize 事件,給回撥函式傳入視窗的文件顯示區的寬度 window.innerWidth。因為繫結事件之後需要銷燬,所以在 unbind 函式內銷燬繫結的事件,要銷燬繫結的事件首先需要獲得繫結的事件,所以,在inserted 鉤子函式內將回調函式放到 el 上,之所以放到 el 上,是因為 el 外的引數都是隻讀的。給 el 新建屬性 _onResize,將回調函式賦給 _onResize,然後在 unbind 函式中移除事件處理程式,刪除 el._onResize。( addEventListener方法相關筆記:https://www.cnblogs.com/xiaoxuStudy/p/13128639.html#three)
在SDirectivePage.vue 的模板裡使用自定義指令 v-resize、定義 v-resize 指令的表示式 onResize, onResize 是一個方法。
<template>
<div>
<s-directive-page />
</div>
</template>
<script>
import SDirectivePage from "./components/SDirectivePage"
export default{
components: {
"s-directive-page": SDirectivePage
}
}
</script>
App.vue
import Vue from 'vue'
import App from './App.vue'
Vue.directive("resize", {
inserted(el, binding){
//將回調函式從引數 binding 中取出來
const callback = binding.value;
//監聽瀏覽器的 resize 事件
window.addEventListener("resize", ()=>{callback(window.innerWidth)});
//可以把共享的資料放到 el 上,因為 el 外的引數都是隻讀的
el._onResize = callback;
},
//繫結事件的話必須將其銷燬,在 unbind 函式內銷燬
unbind(el){
if(!el._onResize) return;
window.removeEventListener("resize", el._onResize);
delete el._onResize;
}
});
new Vue({
render: h => h(App),
}).$mount('#app')
main.js
為了熟悉鉤子函式引數,順便在 SDirectivePage.vue 上標出了鉤子函式的引數的對應值。
<template>
<div v-resize="onResize">window width is: {{ length }}</div>
</template>
<script>
export default {
data(){
return{
direction: "vertical",
length: 0
};
},
methods:{
onResize(length) {
this.length = length;
}
}
}
</script>>
SDirectivePage.vue
頁面表現:
一開始,顯示的數值是 0
如果,拖動視窗邊邊改變高度,數值會變為視窗寬度,數值不會隨著視窗高度的改變而改變,數值會一直都是視窗寬度
如果,拖動視窗邊邊改變寬度,數值會隨著視窗的寬度改變而改變
至此,已經完成一個 v-resize 指令,去監聽瀏覽器大小的改變,改變的時候通過監聽 onResize 響應。然後,將寬度列印到頁面上。
如果想要監聽高度的改變,需要將 main.js 第 9 行傳入的引數由 window.innerWidth 改為 window.innerHeight。
需求2
實現的方式與需求 1 大致相同。
在 main.js 中通過 binding.arg 獲得指令的引數 direction,寫一個函式 result, 返回值是 direction, 判斷 direction 的值是不是 "veritcal",如果是的話, 返回高度,否則,返回寬度。將 result 的返回值作為引數傳入到回撥函式 callback 即 onResize 中。
指令的引數 binding.arg 是 SDirectivePage.vue 模板中 <divv-resize:[direction]="onResize">windowheightis:{{length}}</div> 中的 [direction]。在 SDirectivePage.vue 中可以在 data 中設定 direction 的值,本例中設定成了 vertical。
import Vue from 'vue' import App from './App.vue' Vue.directive("resize", { inserted(el, binding){ const callback = binding.value; // 從引數 binding 中取出指令的引數 const direction = binding.arg; // 定義一個函式判斷 direction 是不是 vertical ,如果是 direction 就是 // window.innerHeight, 否則是 window.innerWidth const result = () => { return direction === "vertical" ? window.innerHeight : window.innerWidth; } const onResize = () => callback(result()); window.addEventListener("resize", onResize); el._onResize = onResize; }, unbind(el){ if(!el._onResize) return; window.removeEventListener("resize", el._onResize); delete el._onResize; } }); new Vue({ render: h => h(App), }).$mount('#app')main.js
<template>
<div v-resize:[direction].quiet="onResize">window height is:{{ length }}</div>
</template>
<script>
export default {
data(){
return{
direction: "vertical",
length: 0
};
},
methods:{
onResize(length) {
this.length = length;
}
}
}
</script>
SDirection.vue
頁面表現:
一開始,顯示的數值是 0
如果,拖動視窗邊邊改變寬度,數值會變為視窗高度,數值不會隨著視窗寬度的改變而改變,數值會一直都是視窗高度。
因為 SDirectionPage.vue 中 direction 的值是 vertical ,所以只監聽高度變化。
如果,拖動視窗邊邊改變高度,數值會隨著視窗的高度改變而改變
至此,已經完成了自定義一個引數 direction, 控制監聽頁面高度的變化。
如果想要監聽寬度的改變,需要將 SDirectivePage.vue 中第 9 行 direction 的值修改為 vertical 以外的值。
需求3
首先解釋一下需求的意思,新增完成一個修飾符 .quiet,來控制是否在指令初始化的時候響應 onResize 函式。在前面的例子中,頁面載入時,顯示的數值是0,現在要實現如果使用了 .quiet 修飾符,則頁面載入時顯示的數值是0,如果沒有使用 .quiet 修飾符,顯示的數值是寬度或者高度。
在需求2的基礎上實現需求3。
在 main.js 中,通過 binding 取得一個包含修飾符的物件。如果在 SDirectivePage.vue 使用修飾符的語句是 <divv-resize:[direction].quiet ="onResize">windowheightis:{{length}}</div> ,那麼 binding.modifiers 是 {quiet: true},如果沒有使用修飾符,語句是 <divv-resize:[direction]="onResize">windowheightis:{{length}}</div> ,那麼 binding.modifiers 是空物件{}。取得 binding.modifiers 之後判斷它是否為空,如果它非空或者說它不包含 quiet 屬性,那麼就執行 onResize 函式,這個 onResize 函式不是在 SDirectivePage.vue 定義的那個,是在 main.js 重新定義的,執行 onReszie 的作用是在頁面上顯示寬度或者高度的數值。
import Vue from 'vue' import App from './App.vue' Vue.directive("resize", { inserted(el, binding){ const callback = binding.value; const direction = binding.arg; // 從引數 binding 中取出一個包含修飾符的物件 modifiers const modifiers = binding.modifiers; const result = () => { return direction === "vertical" ? window.innerHeight : window.innerWidth; } const onResize = () => callback(result()); window.addEventListener("resize", onResize); if(!modifiers || !modifiers.quiet){ onResize(); } el._onResize = onResize; }, unbind(el){ if(!el._onResize) return; window.removeEventListener("resize", el._onResize); delete el._onResize; } }); new Vue({ render: h => h(App), }).$mount('#app')main.js
<template> <div v-resize:[direction].quiet="onResize">window height is: {{ length }}</div> <!-- <div v-resize:[direction]="onResize">window height is: {{ length }}</div> --> </template> <script> export default { data(){ return{ direction: "vertical", length: 0 }; }, methods:{ onResize(length) { this.length = length; } } } </script>SDirectivePage.vue
頁面表現:
1. 使用了 .quiet 修飾符時,初始顯示的數值為 0
2. 不使用 .quite 修飾符時,初始顯示的數值為寬度或者高度,這個例子顯示的是高度