在element-ui源碼中運(yùn)用了四個(gè)指令,分別為點(diǎn)擊元素外,滾輪事件優(yōu)化,單擊事件優(yōu)化,獲取ref指令。這些指令在平時(shí)的開發(fā)中也會(huì)經(jīng)常用到,下面就來一一介紹這些指令的實(shí)現(xiàn)方式以及用途。
1.什么是指令
在理解element-ui中相關(guān)的指令前,先來了解下什么是指令,內(nèi)置指令以及怎么創(chuàng)建自定義指令。
1.1 指令概念
vue中指令都是以v-開頭,作用于html標(biāo)簽,提供一些特殊的特性,當(dāng)指令被綁定到html元素的時(shí)候,指令會(huì)為被綁定的元素添加一些特殊的行為,可以將指令看成html的一種屬性,用于操作DOM。
1.2 內(nèi)置指令
vue中提供了一些內(nèi)置指令,如下所示:
v-text:更新元素的textContent。v-html:更新元素的innerHTML。v-show:根據(jù)表達(dá)式之真假值,切換元素的display CSS property。v-if:條件渲染,用于判斷是否顯示元素。在切換時(shí)元素及它的數(shù)據(jù)綁定 / 組件被銷毀并重建v-else:配合v-if一起使用。v-else-if:配合v-if一起使用。v-for:基于源數(shù)據(jù)多次渲染元素或模板塊。v-on:綁定事件監(jiān)聽器。v-bind:動(dòng)態(tài)地綁定一個(gè)或多個(gè)attribute,或一個(gè)組件prop到表達(dá)式。v-model:在表單控件或者組件上創(chuàng)建雙向綁定。v-slot:提供具名插槽或需要接收 prop 的插槽。v-pre:跳過這個(gè)元素和它的子元素的編譯過程??梢杂脕盹@示原始Mustache標(biāo)簽。跳過大量沒有指令的節(jié)點(diǎn)會(huì)加快編譯。v-cloak:這個(gè)指令保持在元素上直到關(guān)聯(lián)實(shí)例結(jié)束編譯。v-once:只渲染元素和組件一次。隨后的重新渲染,元素/組件及其所有的子節(jié)點(diǎn)將被視為靜態(tài)內(nèi)容并跳過。這可以用于優(yōu)化更新性能。
1.3 自定義指令
Vue 推崇數(shù)據(jù)驅(qū)動(dòng)視圖的理念(數(shù)據(jù)交互,狀態(tài)管理),但并非所有情況都適合數(shù)據(jù)驅(qū)動(dòng)( DOM 的操作)。自定義指令就是一種有效的補(bǔ)充和擴(kuò)展,不僅可用于定義任何的 DOM 操作,并且是可復(fù)用的。
1.3.1 指令定義
使用Vue.directive(id,definition)可以進(jìn)行指令定義。
- id:指令
id,定義好后,可以直接通過v-{id}來使用。- definition:對(duì)象,該對(duì)象提供了一些鉤子函數(shù)
1.3.2 鉤子函數(shù)
一個(gè)指令定義對(duì)象可以提供如下幾個(gè)鉤子函數(shù):
- bind:只調(diào)用一次,指令第一次綁定到元素時(shí)調(diào)用。在這里可以進(jìn)行一次性的初始化設(shè)置。
- inserted:被綁定元素插入父節(jié)點(diǎn)時(shí)調(diào)用 。
- update:所在組件的 VNode 更新時(shí)調(diào)用,但是可能發(fā)生在其子 VNode 更新之前。指令的值可能發(fā)生了改變,也可能沒有。
- componentUpdated:指令所在組件的
VNode及其子VNode全部更新后調(diào)用。- unbind:只調(diào)用一次,指令與元素解綁時(shí)調(diào)用。
1.3.3 鉤子函數(shù)參數(shù)
指令鉤子函數(shù)會(huì)被傳入以下參數(shù):
-
el:指令所綁定的元素,可以用來直接操作 DOM。 -
binding:一個(gè)對(duì)象,包含以下 property:-
name:指令名,不包括v-前綴。 -
value:指令的綁定值。 -
oldValue:指令綁定的前一個(gè)值,僅在update和componentUpdated鉤子中可用。無論值是否改變都可用。 -
expression:字符串形式的指令表達(dá)式。 -
arg:傳給指令的參數(shù),可選。 -
modifiers:一個(gè)包含修飾符的對(duì)象。
-
-
vnode:Vue 編譯生成的虛擬節(jié)點(diǎn)。 -
oldVnode:上一個(gè)虛擬節(jié)點(diǎn),僅在update和componentUpdated鉤子中可用。
2.點(diǎn)擊元素邊界外
該指令主要是用于判斷點(diǎn)擊的點(diǎn)是否在綁定元素的范圍內(nèi)。該指令一般用在彈窗中,如經(jīng)常用到的Popover組件,下拉搜索等。具體實(shí)現(xiàn)思路如下所示:
- 添加
v-clickoutside="close"指令- 2.為
document添加鼠標(biāo)按下和彈起的事件。- 3.綁定元素,并創(chuàng)建相應(yīng)的鼠標(biāo)事件函數(shù)。
- 監(jiān)聽鼠標(biāo)彈起事件,判斷點(diǎn)擊的點(diǎn)是否在元素外。
- 執(zhí)行
v-clickoutside綁定的close事件。
import Vue from 'vue';
import { on } from 'element-ui/src/utils/dom';
const nodeList = [];
const ctx = '@@clickoutsideContext';
let startClick;
let seed = 0;
// 添加鼠標(biāo)按下的事件,并緩存event
!Vue.prototype.$isServer && on(document, 'mousedown', e => (startClick = e));
// 添加鼠標(biāo)點(diǎn)擊后彈起的事件,遍歷nodeList,執(zhí)行nodeList中元素添加的事件
!Vue.prototype.$isServer && on(document, 'mouseup', e => {
nodeList.forEach(node => node[ctx].documentHandler(e, startClick));
});
// 創(chuàng)建元素的點(diǎn)擊事件
function createDocumentHandler(el, binding, vnode) {
// 以彈起和彈出作參數(shù)
return function(mouseup = {}, mousedown = {}) {
// 先判斷點(diǎn)擊的對(duì)象是否為指令綁定的元素本身或空元素對(duì)象
if (!vnode ||
!vnode.context ||
!mouseup.target ||
!mousedown.target ||
el.contains(mouseup.target) ||
el.contains(mousedown.target) ||
el === mouseup.target ||
(vnode.context.popperElm &&
(vnode.context.popperElm.contains(mouseup.target) ||
vnode.context.popperElm.contains(mousedown.target)))) return;
// 獲取指令的表達(dá)式
if (binding.expression &&
el[ctx].methodName &&
// 執(zhí)行指令綁定的方法
vnode.context[el[ctx].methodName]) {
vnode.context[el[ctx].methodName]();
} else {
el[ctx].bindingFn && el[ctx].bindingFn();
}
};
}
/**
* v-clickoutside
* @desc 點(diǎn)擊元素外面才會(huì)觸發(fā)的事件
* @example
* ```vue
* <div v-element-clickoutside="handleClose">
* ```
*/
export default {
bind(el, binding, vnode) {
nodeList.push(el);//將綁定的元素對(duì)象添加到數(shù)組中
const id = seed++;
// 給綁定的元素對(duì)象添加點(diǎn)擊觸發(fā)的方法,使用一個(gè)變量存儲(chǔ)
el[ctx] = {
id,
documentHandler: createDocumentHandler(el, binding, vnode),
methodName: binding.expression,
bindingFn: binding.value
};
},
// 更新
update(el, binding, vnode) {
el[ctx].documentHandler = createDocumentHandler(el, binding, vnode);
el[ctx].methodName = binding.expression;
el[ctx].bindingFn = binding.value;
},
// 解除綁定
unbind(el) {
let len = nodeList.length;
for (let i = 0; i < len; i++) {
if (nodeList[i][ctx].id === el[ctx].id) {
nodeList.splice(i, 1);
break;
}
}
delete el[ctx];
}
};
3.單擊事件優(yōu)化
在src/directives目錄下有一個(gè)repeat-click.js文件,該文件就是一個(gè)用于優(yōu)化單擊事件的指令,我們平時(shí)點(diǎn)擊時(shí),正常的點(diǎn)擊邏輯是這樣的:當(dāng)用戶按住鼠標(biāo)左鍵時(shí),會(huì)觸發(fā)mousedown的回調(diào)。但當(dāng)一直按住鼠標(biāo)左鍵不松手時(shí),就不會(huì)觸發(fā)mousedown的回調(diào),使用該指令就是為了實(shí)現(xiàn)一直按住鼠標(biāo)左鍵不松手時(shí),也能執(zhí)行對(duì)應(yīng)的事件,這指令主要用在InputNumber組件中,當(dāng)鼠標(biāo)點(diǎn)擊-或+不松開時(shí),數(shù)字可以持續(xù)的進(jìn)行加減。下面就來看看指令是怎么實(shí)現(xiàn)的:
- 引入指令文件,
import RepeatClick from 'element-ui/src/directives/repeat-click';
- 在
directives中注冊(cè)指令。
- 使用指令,
v-repeat-click="decrease"
- 在
bind事件中定義一個(gè)clear的函數(shù),用于清除定時(shí)器。- 5.為綁定指令的元素添加
moursedown事件。- 6.在
moursedown回調(diào)事件中,定義一個(gè)定時(shí)器,每100秒執(zhí)行一次回調(diào)函數(shù)。
- 鼠標(biāo)彈起時(shí),執(zhí)行
clear函數(shù)清除定時(shí)器。
import { once, on } from '@/utils/dom';
export default {
bind(el, binding, vnode) {
let interval = null;
let startTime;
// 獲取指令綁定的事件函數(shù)
const handler = () => vnode.context[binding.expression].apply();
// 定義一個(gè)清除定時(shí)器的函數(shù)
const clear = () => {
// 間隔時(shí)間小于100毫秒時(shí),繼續(xù)執(zhí)行回調(diào)函數(shù)
if (Date.now() - startTime < 100) {
handler();
}
clearInterval(interval);
interval = null;
};
// 添加點(diǎn)擊事件
on(el, 'mousedown', (e) => {
if (e.button !== 0) return;
// 緩存點(diǎn)擊時(shí)的時(shí)間
startTime = Date.now();
// 添加鼠標(biāo)彈起的事件
once(document, 'mouseup', clear);
// 清除定時(shí)器
clearInterval(interval);
// 100毫秒執(zhí)行一次回調(diào)函數(shù)
interval = setInterval(handler, 100);
});
}
};
4.滾輪事件優(yōu)化
在src/directives目錄下有一個(gè)mousewheel.js文件,該指令主要是對(duì)鼠標(biāo)滾動(dòng)事件進(jìn)行了優(yōu)化,使用normalize-wheel這個(gè)庫來解決不同瀏覽器之間的兼容性來獲取x方向和y方向的滾動(dòng)偏移量。
import normalizeWheel from 'normalize-wheel';
const isFirefox = typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
const mousewheel = function(element, callback) {
if (element && element.addEventListener) {
element.addEventListener(isFirefox ? 'DOMMouseScroll' : 'mousewheel', function(event) {
const normalized = normalizeWheel(event);
callback && callback.apply(this, [event, normalized]);
});
}
};
export default {
bind(el, binding) {
mousewheel(el, binding.value);
}
};
5.獲取ref指令
在packages/popover/src/目錄下有一個(gè)directive.js文件,該指令主要是用于Popover組件,用于獲取Popover組件的ref,
const getReference = (el, binding, vnode) => {
const _ref = binding.expression ? binding.value : binding.arg;
const popper = vnode.context.$refs[_ref];
if (popper) {
if (Array.isArray(popper)) {
popper[0].$refs.reference = el;
} else {
popper.$refs.reference = el;
}
}
};
export default {
bind(el, binding, vnode) {
getReference(el, binding, vnode);
},
inserted(el, binding, vnode) {
getReference(el, binding, vnode);
}
};
總結(jié)
element-ui的指令基本上都介紹完了,平時(shí)開發(fā)的時(shí)候除了使用vue內(nèi)置的指令外,應(yīng)該盡可能多封裝一些組件來提升工作效率。