随着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];
}
};
复制代码