实现一个可复用的点击区域之外方法

随着3大框架的风靡,我们从以前的layer等UI库迁移到了更加强大的UI库,比如vue的好伙伴element,组件库的作用是封装一些常用的功能,将HTML、CSS、JS作为一个功能单元封装为一个整体,向外界暴露合理的接口,它极大地提升了我们的开发效率,最近遇到一个要自己写一个select(选择器)场景,以下的场景一下子让我懵了

比如上图的选择器,我们除了点击输入框时,会切换列表展开状态,点击列表项会收起列表,同时,我们需要在点击其他区域时,也要关闭列表,本文基于此需求展开

DOM判断

这个需求最重要的点就是需要判断点击区域在指定区域之外,执行指定的逻辑,沿着这个思路,我竟然想去了去计算当前点击的坐标是否在指定区域,这显然是不行的,从视觉上难以判断,有没有能够从编码上判断的方法呢,比如,判断点击的DOM不是指定的DOM,于是有了第一版的方案

// 给元素绑定click事件
element.addEventListener("click",(e) => {
const { target } = e // 判断target是不是在指定DOM
}, false);

这里有两个严重的问题

  • 按照上述代码,需要为页面上的每个DOM元素都绑定一个事件,无论在代码量和性能上,都十分不好

  • 指定DOM只能是满足条件的,要是比较多,会导致这部分逻辑很复杂

事件委托

原先的代码,会导致绑定和事件在每个DOM节点上重复,其实,程序只需要知道本次点击的是谁,不需要关注绑定事件的是谁,这个时候,我们的事件委托就上场了。

// 给元素绑定click事件
document.addEventListener("click",(e) => {
const { target } = e // 判断target是不是在指定DOM
}, false);

我们将事件绑定在document上,默认情况下,事件是遵循冒泡模型,从事件源往document上触发对应类型的事件,所以事件点击时候,能够在doucument上统一接受到事件源,另外,这里测试了一下,因为在某些场景下,可能会使用到捕获模型

<!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事件,但是这个可以接受

contains

我们一开始是判断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中对应的逻辑

v-clickout指令实现

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];
}
};

探索clickout指令实现的更多相关文章

  1. 走进AngularJs(三)自定义指令-----(上)

    一.有感而发的一些话 在学习ng之前有听前辈说过,angular上手比较难,初学者可能不太适应其语法以及思想.随着对ng探索的一步步深入,也确实感觉到了这一点,尤其是框架内部的某些执行机制,其复杂程度 ...

  2. JavaScript修改Canvas图片

    用JavaScript修改Canvas图片的分辨率(DPI)   应用场景: 仓库每次发货需要打印标签, Canvas根据从数据库读取的产品信息可以生成标签JPG, 但是这个JPG图片的默认分辨率(D ...

  3. 【转】: 探索Lua5.2内部实现:虚拟机指令(3) Upvalues & Globals

    在编译期,如果要访问变量a时,会依照以下的顺序决定变量a的类型: a是当前函数的local变量 a是外层函数的local变量,那么a是当前函数的upvalue a是全局变量 local变量本身就存在于 ...

  4. 【转】: 探索Lua5.2内部实现:虚拟机指令(2) MOVE & LOAD

    name args desc OP_MOVE A B R(A) := R(B) OP_MOVE用来将寄存器B中的值拷贝到寄存器A中.由于Lua是register based vm,大部分的指令都是直接 ...

  5. 【转】: 探索Lua5.2内部实现:虚拟机指令(1) 概述

    Lua一直把虚拟机执行代码的效率作为一个非常重要的设计目标.而采用什么样的指令系统的对于虚拟机的执行效率来说至关重要. Stack based vs Register based VM 根据指令获取操 ...

  6. 【探索】机器指令翻译成 JavaScript

    前言 前些时候研究脚本混淆时,打算先学一些「程序流程」相关的概念.为了不因太枯燥而放弃,决定想一个有趣的案例,可以边探索边学. 于是想了一个话题:尝试将机器指令 1:1 翻译 成 JavaScript ...

  7. 【系统篇】从int 3探索Windows应用程序调试原理

    探索调试器下断点的原理 在Windows上做开发的程序猿们都知道,x86架构处理器有一条特殊的指令——int 3,也就是机器码0xCC,用于调试所用,当程序执行到int 3的时候会中断到调试器,如果程 ...

  8. NoSQL初探之人人都爱Redis:(4)Redis主从复制架构初步探索

    一.主从复制架构简介 通过前面几篇的介绍中,我们都是在单机上使用Redis进行相关的实践操作,从本篇起,我们将初步探索一下Redis的集群,而集群中最经典的架构便是主从复制架构.那么,我们首先来了解一 ...

  9. 探索c#之递归APS和CPS

    接上篇探索c#之尾递归编译器优化 累加器传递模式(APS) CPS函数 CPS变换 CPS尾递归 总结 累加器传递模式(Accumulator passing style) 尾递归优化在于使堆栈可以不 ...

随机推荐

  1. Paper | LISTEN, ATTEND AND SPELL: A NEURAL NETWORK FOR LARGE VOCABULARY CONVERSATIONAL SPEECH RECOGNITION

    目录 1. 相关工作 2. 方法细节 2.1 收听器 2.2 注意力和拼写 本文提出了一个基于神经网络的语音识别系统List, Attend and Spell(LAS),能够将语音直接转录为文字. ...

  2. Apollo服务端设计原理剖析

    本文摘自于<Spring Cloud微服务 入门 实战与进阶>一书. 1 配置发布后的实时推送设计 配置中心最重要的一个特性就是实时推送了,正因为有这个特性,我们可以依赖配置中心做很多事情 ...

  3. 1+x 证书 Web 前端开发 css 专项练习

    官方QQ群 1+x 证书 Web 前端开发 css 专项练习 http://blog.zh66.club/index.php/archives/192/

  4. POJ 1724 (分层图最短路)

    ### POJ 1724 题目链接 ### 题目大意: 给你 N 个点 ,M 条有向路,走每条路需要花费 C 元,这段路的长度为 L . 给你 K 元,问你能否从 1 走到 N 点且花费不超过 K 元 ...

  5. ARM64的内核栈、用户栈、寄存器上下文

    1. 内核栈的分配,即thread_info的分配,是在do_fork->dup_task_struct中分配(默认为2个pages),并赋值给task_struct->stack: 2. ...

  6. pandas.read_sql_query()读取数据库数据用chunksize的坑

    最近一项工作需要读取数据库中1500万条数据,考虑到数据量太大,不方便直接一次性读取,不然会内存爆炸.想到用pandas.read_sql_query()里有一个chunksize可以分批返回chun ...

  7. 英语阅读——Speaking Chinese in America

    这篇文章是<新视野大学英语>第四册的第五单元的文章,第一遍英语阅读完后对比中文,发现自己对作者的观点理解有些出入.作者反对的是认为中国说话客套而美国人直接的观点,利用自己的经历表达了中文也 ...

  8. Netty—TCP的粘包和拆包问题

    一.前言 虽然TCP协议是可靠性传输协议,但是对于TCP长连接而言,对于消息发送仍然可能会发生粘贴的情形.主要是因为TCP是一种二进制流的传输协议,它会根据TCP缓冲对包进行划分.有可能将一个大数据包 ...

  9. C# shell32.dll 的用法

    1 首先要使用shell32  请在项目引用中添加shell32.dll 的引用  (备注:该引用是系统dll文件 在C:\Windows\System32  目录下 可以自行拷贝到项目中) priv ...

  10. C#上手练习7(构造方法语句)

    创建类的对象是使用“类名 对象名 = new 类名()”的方式来实现的. 实际上,“类名()”的形式调用的是类的构造方法,也就是说构造方法的名字是与类的名称相同的. 构造方法的定义语法形式如下. 访问 ...