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

随着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. 更新GitHub项目出现There is no tracking information for the current branch. Please specify which branch you want to merge with. 怎么解决

    git pull命令用于从另一个存储库或本地分支获取并集成(整合).git pull命令的作用是:取回远程主机某个分支的更新,再与本地的指定分支合并,它的完整格式稍稍有点复杂. 如果当前分支只有一个追 ...

  2. 【LOJ2402】「THUPC 2017」天天爱射击 / Shooting(整体二分)

    点此看题面 大致题意: 有\(n\)个区间,每个区间有一个权值,当权值变成\(0\)时消失.每个时刻将覆盖某一位置的所有区间权值减\(1\),求每个时刻有多少个区间在这一刻消失. 前言 整体二分裸题啊 ...

  3. A1071 Speech Patterns (25 分)

    一.技术总结 开始拿到这道题目时,思考的是我该如何区分它们每一个单词,不知道这里还是要学习得知在cctype头文件中有一个函数用于查看是否为0~9.a~z.A~Z,就是isalnum(),又因为题目中 ...

  4. 自动编写Python程序的神器,Python 之父都发声力挺!

    ​ 就在不久前,kite——那个能够自己编写python代码的AI,Python 之父 Guido van Rossum 使用之后,也发出了「really love」感叹,向大家墙裂推荐了这一高效工具 ...

  5. #3146. 「APIO 2019」路灯

    #3146. 「APIO 2019」路灯 题目描述 一辆自动驾驶的出租车正在 Innopolis 的街道上行驶.该街道上有 \(n + 1\) 个停车站点,它们将街道划分成了 \(n\) 条路段.每一 ...

  6. C++入门到理解阶段二基础篇(6)——C++数组

    概述 C++ 支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合.数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量. 数组的声明并不是声明一个个单独的变量,比如 numbe ...

  7. efcore dotnet cli add-migrations update-database

    add-migrations update-database 如何通过dotnet cli调用 dotnet tool install --global dotnet-ef dotnet ef mig ...

  8. RabbitMQ的安装与使用(Centos7,linux版本)

    1.主流的消息中间件简单介绍哦. 1).ActiveMQ是Apache出品,最流行的,能力强劲的开源消息总线,并且它一个完全支持jms(java message service)规范的消息中间件.其丰 ...

  9. C# Mutex to make sure only one unique application instance started

    static void MutexDemo2() { string assName = Assembly.GetEntryAssembly().FullName; bool createdNew; u ...

  10. qt构建错误: dependent "*.h" does not exist.

    项目中需要维护一套qt工程,今天发现一个头文件名称中单词拼写错误,就改正了,结果重新构建提示: dependent "*.h" does not exist. 原因:修改了文件后, ...