隨着3大框架的風靡,咱們從之前的layer等UI庫遷移到了更增強大的UI庫,好比vue的好夥伴element,組件庫的做用是封裝一些經常使用的功能,將HTML、CSS、JS做爲一個功能單元封裝爲一個總體,向外界暴露合理的接口,它極大地提高了咱們的開發效率,最近遇到一個要本身寫一個select(選擇器)場景,如下的場景一會兒讓我懵了javascript
好比上圖的選擇器,咱們除了點擊輸入框時,會切換列表展開狀態,點擊列表項會收起列表,同時,咱們須要在點擊其餘區域時,也要關閉列表,本文基於此需求展開html
這個需求最重要的點就是須要判斷點擊區域在指定區域以外,執行指定的邏輯,沿着這個思路,我居然想去了去計算當前點擊的座標是否在指定區域,這顯然是不行的,從視覺上難以判斷,有沒有可以從編碼上判斷的方法呢,好比,判斷點擊的DOM不是指定的DOM,因而有了初版的方案vue
// 給元素綁定click事件
element.addEventListener("click",(e) => {
const { target } = e
// 判斷target是否是在指定DOM
}, false);
複製代碼
這裏有兩個嚴重的問題java
按照上述代碼,須要爲頁面上的每一個DOM元素都綁定一個事件,不管在代碼量和性能上,都十分很差node
指定DOM只能是知足條件的,要是比較多,會致使這部分邏輯很複雜express
原先的代碼,會致使綁定和事件在每一個DOM節點上重複,其實,程序只須要知道本次點擊的是誰,不須要關注綁定事件的是誰,這個時候,咱們的事件委託就上場了。框架
// 給元素綁定click事件
document.addEventListener("click",(e) => {
const { target } = e
// 判斷target是否是在指定DOM
}, false);
複製代碼
咱們將事件綁定在document上,默認狀況下,事件是遵循冒泡模型,從事件源往document上觸發對應類型的事件,因此事件點擊時候,可以在doucument上統一接受到事件源,另外,這裏測試了一下,由於在某些場景下,可能會使用到捕獲模型ide
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<button id="button"></button>
</body>
<script> // 註冊捕獲階段觸發的事件 document.getElementById("button").addEventListener("click", () => { }, true) // 代理document內元素的click document.addEventListener("click", (e)=> { const { target } = e console.log(target); }) </script>
</html>
複製代碼
能夠看到,即便使用了捕獲模型,咱們的事件源也是同樣能夠正確獲取,固然,這只是一個嘗試,通常來講,這個不用嘗試都知道是這樣的,利用事件委託,咱們很好地減小了事件綁定重複,有一個微不足道的缺點,就是阻止了事件傳播,如阻止事件冒泡的元素不能正確觸發咱們的document事件,可是這個能夠接受性能
咱們一開始是判斷DOM節點特有的標誌來執行咱們的程序測試
// 判斷指定節點
if($(target).attr("id") === xxx)
複製代碼
這樣子太受限了,須要寫不少條件,何況咱們須要的是區域,因此最好可以有個API能判斷是否在一個區域,正好,有一個API
node.contains( otherNode )
node 是否包含otherNode節點.
otherNode 是不是node的後代節點.
複製代碼
contains這個API,能夠判斷一個節點是否包含在另一個節點以內,這個內部是指是否爲判斷節點自己或者其後臺節點,因而,咱們利用此API,就能夠完美判斷一個節點是否在一個區域以外
// 觸發事件節點在區域外
!node.contains(target)
複製代碼
最終的代碼是
document.addEventListener("click",(e) => {
const { target } = e
if(!node.contains(target)) {
// 點擊區域以外的事情
}
}, false);
複製代碼
上述的最終代碼已經能夠用了,可是對於多個元素來講,他們須要不一樣的callback,這裏咱們咱們進行一個改造
let nodeList = []
document.addEventListener("click",(e) => {
const { target } = e
nodeList.map(({node, cb}) => {
if(!node.contains(target)) {
cb()
// 點擊區域以外的事情
}
})
}, false);
// 將你須要實現點擊區域以外的邏輯置入nodeList之中
nodeList.push({
node: node,
cb: function () {
}
})
複製代碼
因爲節點和callback是每一個須要此交互的都不一樣,這裏講節點和callback存儲到一個全局的列表中去,而後點擊頁面時,去觸發列表中點擊元素不在其指定範圍的callback,使得邏輯得以複用,而每一個元素本身的業務邏輯能夠分離,不過這裏有個小問題就是要注意在指定節點移除時,要及時手動移除nodeList中對應的邏輯
這部分原本想着本身寫的,而後忽然想到element可能實現了,藉助Vue的指令,咱們能更方便的管理DOM的生命週期,輕鬆添加、銷燬callback,下面是element的clickout指令源碼,思路大體相同,只是基於vue實現
const on = (function() {
if (document.addEventListener) {
return function(element, event, handler) {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
};
} else {
return function(element, event, handler) {
if (element && event && handler) {
element.attachEvent('on' + event, handler);
}
};
}
})();
const nodeList = [];
const ctx = '@@clickoutsideContext';
let startClick;
let seed = 0;
on(document, 'mousedown', e => (startClick = e));
on(document, 'mouseup', e => {
nodeList.forEach(node => node[ctx].documentHandler(e, startClick));
});
function createDocumentHandler(el, binding, vnode) {
return function(mouseup = {}, mousedown = {}) {
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;
if (binding.expression &&
el[ctx].methodName &&
vnode.context[el[ctx].methodName]) {
vnode.context[el[ctx].methodName]();
} else {
el[ctx].bindingFn && el[ctx].bindingFn();
}
};
}
/** * v-clickoutside * @desc 點擊元素外面纔會觸發的事件 * @example * ```vue * <div v-element-clickoutside="handleClose"> * ``` */
export default {
bind(el, binding, vnode) {
nodeList.push(el);
const id = seed++;
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];
}
};
複製代碼