用 JavaScript 实现手势库 — 事件派发与 Flick 事件【前端组件化】

前端《组件化系列》目录
- 「一」用 JSX 建立组件 Parser(解析器)
- 「二」使用 JSX 建立 Markup 组件风格
- 「三」用 JSX 实现 Carousel 轮播组件
- 「四」用 JavaScript 实现时间轴与动画
- 「五」用 JavaScript 实现三次贝塞尔动画库 - 前端组件化
- 「六」用 JavaScript 实现手势库 - 实现监听逻辑
- 「七」用 JavaScript 实现手势库 — 手势逻辑
- 「八」用 JavaScript 实现手势库 — 支持多键触发
- 「九」用 JavaScript 实现手势库 — 事件派发与 Flick 事件 《 本期 》
- … 待续 …
我们上一期已经实现了所有的 gesture(手势),接下来我们需要实现的就是事件派发的功能。
事件派发
在 DOM 里面事件的派发是使用 new Event , 然后在上面加一些属性,最后把这个事件给派发出去的。
所以我们这里也是一样,建立一个 dsipatch 的函数,并且加入 type、property 这些参数。这里的 property 含有 context 对象和 point 坐标两个属性。
在我们的 dispatch 函数中,首先我们需要做的就是创建一个 event 对象。在新的浏览器 API 中,我们可以直接使用 new Event 来创建。当然我们也可以使用自定义事件来创建 new CustomEvent。那么我们这里,就用普通的 new Event 就好了。
function dispatch(type, properties) {
let event = new Event(type);
}
然后我们循环一下 properties 这个对象,把里面的属性都抄写一下。然后我们新创建的 event 是需要挂在一个元素上面,把它挂在到我们之前定义的 element 上即可。
function dispatch(type, properties) {
let event = new Event(type);
for (let name in properties) {
event[name] = properties[name];
}
element.dispatchEvent(event);
}
这里其实还有一个问题,就是我们之前写的监听都是挂载在 element 之上的。最后我们要把这些都换成挂载在 document 上。
element.addEventListener('mousedown', event => {
let context = Object.create(null);
contexts.set(`mouse${1 << event.button}`, context);
start(event, context);
let mousemove = event => {
let button = 1;
while (button <= event.buttons) {
if (button & event.buttons) {
let key;
// Order of buttons & button is not the same
if (button === 2) {
key = 4;
} else if (button === 4) {
key = 2;
} else {
key = button;
}
let context = contexts.get('mouse' + key);
move(event, context);
}
button = button << 1;
}
};
let mouseup = event => {
let context = contexts.get(`mouse${1 << event.button}`);
end(event, context);
contexts.delete(`mouse${1 << event.button}`);
if (event.buttons === 0) {
document.removeEventListener('mousemove', mousemove);
document.removeEventListener('mouseup', mouseup);
isListeningMouse = false;
}
};
if (!isListeningMouse) {
document.addEventListener('mousemove', mousemove);
document.addEventListener('mouseup', mouseup);
isListeningMouse = true;
}
});
然后我们来把 end 函数中的 tap 事件 dipatch(派发)出来试试:
let end = (point, context) => {
if (context.isTap) {
//console.log('tap');
// 把原先的 console.log 换成 dispatch 调用
// 这个事件不需要任何特殊属性,直接传`空对象`即可
dispatch('tap', {})
clearTimeout(context.handler);
}
if (context.isPan) {
console.log('pan-end');
}
if (context.isPress) {
console.log('press-end');
}
};
那么最后,我们可以尝试在 HTML 中加入一个脚本,在里面监听一下我们新创建的 tap 事件。
<script src="gesture.js"></script>
<body oncontextmenu="event.preventDefault()"></body>
<script>
document.documentElement.addEventListener('tap', () => {
console.log('Tapped!');
});
</script>
这个时候,如果我们去浏览器上点击一下,就会触发我们的 tap 事件,并且输出我们的 'Tapped' 消息了!

这样我们的派发事件就大功告成了。
实现一个 flick 事件
这里我们一起来完成最后一个最特别的 flick 事件。Flick 事件在我们所有的事件体系里是比较特殊的,因为它是一个需要判断数独的一个事件。
根据我们前面讲到的,在 pan start 之后,如果我们在手指离开屏幕之前,我们执行了一个快速滑动手指的动作,到达一定的速度以上就会触发我们的 flick 事件,而不是原本的 pan end 的事件。
那么需要如何判断这个速度的?其实可以在我们的 move 函数中,获得当前这一次移动时的速度。但是这个并不能帮助我们去处理,因为如果只按照两个点之间移动时的速度,根据浏览器实现的不同,它会有一个较大的误差。
所以更加准确的方式就是,取数个点,然后用它们之间的平均值作为判定的值。那么要实现这个功能,我们就需要存储一段时间之内的这些点,然后使用这些点来计算出速度的平均值。
有了实现的思路了,我们就来整理下,在代码中怎么去编写这一块的逻辑。
首先我们需要在触发 start 的时候,就把第一个记录点加入到我们的全局 context 之中。而这里需要记录几个值:
t:代表当前点触发/加入时的时间,这里我们使用Date.now()x:代表当前点 x 轴的坐标y:代表当前点 y 轴的坐标
这些值到了后面都会用来计算移动速度的。
let start = (point, context) => {
(context.startX = point.clientX), (context.startY = point.clientY);
context.points = [
{
t: Date.now(),
x: point.clientX,
y: point.clientY,
},
];
context.isPan = false;
context.isTap = true;
context.isPress = false;
context.handler = setTimeout(() => {
context.isPan = false;
context.isTap = false;
context.isPress = true;
console.log('press-start');
context.handler = null;
}, 500);
};
然后每一次触发 move 的时候,都给当前的 content 放入一个新的点。但是在加入新的点之前,需要过滤一次已经存储的点。我们只需要最近 500 毫秒内的点来计算速度即可,其余的点就可以过滤掉了。
在执行 flick 动作的时候,我们是不会滑动一个很长的距离和时间的,加上我们是需要捕捉一个快速的滑动动作,这个动作肯定是在 500 毫秒以内的动作,要不也不叫 “快” 了。所以这里就只需要 500 毫秒内的点即可。
let move = (point, context) => {
let dx = point.clientX - context.startX,
dy = point.clientY - context.startY;
if (!context.isPan && dx ** 2 + dy ** 2 > 100) {
context.isPan = true;
context.isTap = false;
context.isPress = false;
console.log('pan-start');
clearTimeout(context.handler);
}
if (context.isPan) {
console.log(dx, dy);
console.log('pan');
}
context.points = context.points.filter(point => Date.now() - point.t < 500);
context.points.push({
t: Date.now(),
x: point.clientX,
y: point.clientY,
});
};
在 end 事件触发的时候,就可以来计算这次滑动的速度了。因为这里是计算用户滑动时的速度,如果用户是其他类型的手势动作,是不需要去计算速度的。所以这段计算逻辑就可以写在 isPan 成立的判断里面即可。
首先给这个手势动作一个状态变量 isFlick,并且给予它一个默认值为 false。
在计算速度之前,一样需要过滤一次我们 context 中储存的全部的点,把 500 毫秒之外的点过滤掉。
在数学或者物理中,有一个计算速度的公式: 速度 = 距离 / 用时。那么这里要去计算速度的话,首先需要计算的就是距离。而这里要计算的是直径距离,所以需要 x 轴和 y 轴的距离的二次幂相加,然后开根号获得的值就是我们要的直径距离。
那么 x 轴距离为例,就是当前点的 x 轴坐标,减去记录中第一个点的 x 轴左边。y 轴的距离就同理可得了。那么有了距离,我们就可以直接从当前点和第一个点的时间差获得 用时。最后就可以运算出速度。
let end = (point, context) => {
context.isFlick = false;
if (context.isTap) {
//console.log('tap');
// 把原先的 console.log 换成 dispatch 调用
// 这个事件不需要任何特殊属性,直接传`空对象`即可
dispatch('tap', {});
clearTimeout(context.handler);
}
if (context.isPan) {
context.points = context.points.filter(point => Date.now() - point.t < 500);
let d = Math.sqrt((point.x - context.points[0].x) ** 2 + (point.y - context.points[0].y) ** 2);
let v = d / (Date.now() - context.points[0].t);
}
if (context.isPress) {
console.log('press-end');
}
};
好样的,这样我们就有两个点之间的 v 速度。那么现在呢,我们需要知道多快的速度才能认为是一个 flick 动作呢?这里就用上帝视角直接得出 1.5 像素每毫秒的速度就是最合适的(这个怎么算出来的?其实我们可以直接 console.log(v),把速度打印出啦,然后我们手动去测试,就会发现大概 v = 1.5 的时候差不多就是对的了)。
所以我们这里直接就可以判断, 如果 v > 1.5 的话,我们就认为用户的手势就是一个 flick,否则就是普通的 pan-end。
let end = (point, context) => {
context.isFlick = false;
if (context.isTap) {
//console.log('tap');
// 把原先的 console.log 换成 dispatch 调用
// 这个事件不需要任何特殊属性,直接传`空对象`即可
dispatch('tap', {});
clearTimeout(context.handler);
}
if (context.isPan) {
context.points = context.points.filter(point => Date.now() - point.t < 500);
let d = Math.sqrt((point.x - context.points[0].x) ** 2 + (point.y - context.points[0].y) ** 2);
let v = d / (Date.now() - context.points[0].t);
if (v > 1.5) {
context.isFlick = true;
dispatch('flick', {});
} else {
context.isFlick = false;
dispatch('panend', {});
}
}
if (context.isPress) {
console.log('press-end');
}
};
这样 flick 事件的处理就完成了,其实这段代码中还有一些 console.log() 是没有被改为使用 dispatch 给派发出去的。但是接下来就要开始看看怎么重新封装这个手势库了,所以这里我们就不一一更改过来先了。
如果想把这里的代码写完整的同学,可以自行把所有的 console.log(事件名) 部分的代码都改正过来哦~
最后附上到此完整的代码。
let element = document.documentElement;
let contexts = new Map();
let isListeningMouse = false;
element.addEventListener('mousedown', event => {
let context = Object.create(null);
contexts.set(`mouse${1 << event.button}`, context);
start(event, context);
let mousemove = event => {
let button = 1;
while (button <= event.buttons) {
if (button & event.buttons) {
let key;
// Order of buttons & button is not the same
if (button === 2) {
key = 4;
} else if (button === 4) {
key = 2;
} else {
key = button;
}
let context = contexts.get('mouse' + key);
move(event, context);
}
button = button << 1;
}
};
let mouseup = event => {
let context = contexts.get(`mouse${1 << event.button}`);
end(event, context);
contexts.delete(`mouse${1 << event.button}`);
if (event.buttons === 0) {
document.removeEventListener('mousemove', mousemove);
document.removeEventListener('mouseup', mouseup);
isListeningMouse = false;
}
};
if (!isListeningMouse) {
document.addEventListener('mousemove', mousemove);
document.addEventListener('mouseup', mouseup);
isListeningMouse = true;
}
});
element.addEventListener('touchstart', event => {
for (let touch of event.changedTouches) {
let context = Object.create(null);
contexts.set(event.identifier, context);
start(touch, context);
}
});
element.addEventListener('touchmove', event => {
for (let touch of event.changedTouches) {
let context = contexts.get(touch.identifier);
move(touch, context);
}
});
element.addEventListener('touchend', event => {
for (let touch of event.changedTouches) {
let context = contexts.get(touch.identifier);
end(touch, context);
contexts.delete(touch.identifier);
}
});
element.addEventListener('cancel', event => {
for (let touch of event.changedTouches) {
let context = contexts.get(touch.identifier);
cancel(touch, context);
contexts.delete(touch.identifier);
}
});
let start = (point, context) => {
(context.startX = point.clientX), (context.startY = point.clientY);
context.points = [
{
t: Date.now(),
x: point.clientX,
y: point.clientY,
},
];
context.isPan = false;
context.isTap = true;
context.isPress = false;
context.handler = setTimeout(() => {
context.isPan = false;
context.isTap = false;
context.isPress = true;
console.log('press-start');
context.handler = null;
}, 500);
};
let move = (point, context) => {
let dx = point.clientX - context.startX,
dy = point.clientY - context.startY;
if (!context.isPan && dx ** 2 + dy ** 2 > 100) {
context.isPan = true;
context.isTap = false;
context.isPress = false;
console.log('pan-start');
clearTimeout(context.handler);
}
if (context.isPan) {
console.log(dx, dy);
console.log('pan');
}
context.points = context.points.filter(point => Date.now() - point.t < 500);
context.points.push({
t: Date.now(),
x: point.clientX,
y: point.clientY,
});
};
let end = (point, context) => {
context.isFlick = false;
if (context.isTap) {
//console.log('tap');
// 把原先的 console.log 换成 dispatch 调用
// 这个事件不需要任何特殊属性,直接传`空对象`即可
dispatch('tap', {});
clearTimeout(context.handler);
}
if (context.isPan) {
context.points = context.points.filter(point => Date.now() - point.t < 500);
let d, v;
if (!context.points.length) {
v = 0;
} else {
d = Math.sqrt(
(point.clientX - context.points[0].x) ** 2 + (point.clientY - context.points[0].y) ** 2
);
v = d / (Date.now() - context.points[0].t);
}
if (v > 1.5) {
context.isFlick = true;
dispatch('flick', {});
} else {
context.isFlick = false;
dispatch('panend', {});
}
}
if (context.isPress) {
console.log('press-end');
}
};
let cancel = (point, context) => {
clearTimeout(context.handler);
console.log('cancel');
};
function dispatch(type, properties) {
let event = new Event(type);
for (let name in properties) {
event[name] = properties[name];
}
element.dispatchEvent(event);
}
下一期,我们就来做手势库的最后一步,封装!~
我是来自《技术银河》的三钻,一位正在重塑知识的技术人。下期再见。
⭐️ 三哥推荐
开源项目推荐
Hexo Theme Aurora


在最近在版本 1.5.0 更新了以下功能:
》预览《
✨ 新增
- 自适应 “推荐文章” 布局 (增加了一个新的 “
置顶文章布局” !!)- 能够在“推荐文章”和“置顶文章”模式之间自由切换
- 如果总文章少于 3 篇,将自动切换到“置顶文章”模式
- 在文章卡上添加了“置顶”和“推荐”标签
- 📖 文档
- 增加了与 VuePress 一样的自定义容器 #77
Info容器Warning容器Danger容器Detail容器- 预览
- 支持了更多的 SEO meta 数据 #76
- 添加了
description - 添加了
keywords - 添加了
author - 📖 文档
- 添加了

最近博主在全面投入开发一个可以 “迈向未来的” Hexo 主题,以极光为主题的博客主题。
如果你是一个开发者,做一个个人博客也是你简历上的一个亮光点。而如果你有一个超级炫酷的博客,那就更加是亮上加亮了,简直就闪闪发光。
如果喜欢这个主题,可以在 Github 上给我点个 🌟 让彼此都发光吧~
主题 Github 地址:https://github.com/auroral-ui/hexo-theme-aurora
主题使用文档:https://aurora.tridiamond.tech/zh/
VSCode Aurora Future

对,博主还做了一个 Aurora 的 VSCode 主题。用了Hexo Theme Aurora 相对应的颜色色系。这个主题的重点特性的就只用了 3 个颜色,减少在写代码的时候被多色多彩的颜色所转移了你的注意力,让你更集中在写代码之中。
喜欢的大家可以支持一下哦! 直接在 VSCode 的插件搜索中输入 “Aurora Future” 即可找到这个主题哦!~
主题 Github 地址:https://github.com/auroral-ui/aurora-future-vscode-theme
主题插件地址:https://marketplace.visualstudio.com/items?itemName=auroral-ui.aurora-future
Firefox Aurora Future
我不知道大家,但是最近我在用火狐浏览器来做开发了。个人觉得火狐还真的是不错的。推荐大家尝试一下。
当然我这里想给大家介绍的是我在火狐也做了一个 Aurora 主题。对的!用的是同一套的颜色体系。喜欢的小伙伴可以试一下哦!
主题地址:https://addons.mozilla.org/en-US/firefox/addon/aurora-future/