前端《组件化系列》目录
- 「一」用 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 毫秒内的点来计算速度即可,其余的点就可以过滤掉了。
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
:sparkles: 新增
- 自适应 “推荐文章” 布局 (增加了一个新的 “
置顶文章布局
” !!)- 能够在“推荐文章”和“置顶文章”模式之间自由切换
- 如果总文章少于 3 篇,将自动切换到“置顶文章”模式
- 在文章卡上添加了“置顶”和“推荐”标签
- :book: 文档
- 增加了与 VuePress 一样的自定义容器 #77
Info
容器Warning
容器Danger
容器Detail
容器- 预览
- 支持了更多的 SEO meta 数据 #76
- 添加了
description
- 添加了
keywords
- 添加了
author
- :book: 文档
- 添加了
最近博主在全面投入开发一个可以 “迈向未来的” Hexo 主题,以极光为主题的博客主题。
如果你是一个开发者,做一个个人博客也是你简历上的一个亮光点。而如果你有一个超级炫酷的博客,那就更加是亮上加亮了,简直就闪闪发光。
如果喜欢这个主题,可以在 Github 上给我点个 ? 让彼此都发光吧~
VSCode Aurora Future
对,博主还做了一个 Aurora 的 VSCode 主题。用了Hexo Theme Aurora 相对应的颜色色系。这个主题的重点特性的就只用了 3 个颜色,减少在写代码的时候被多色多彩的颜色所转移了你的注意力,让你更集中在写代码之中。
喜欢的大家可以支持一下哦! 直接在 VSCode 的插件搜索中输入 “Aurora Future” 即可找到这个主题哦!~
Firefox Aurora Future
我不知道大家,但是最近我在用火狐浏览器来做开发了。个人觉得火狐还真的是不错的。推荐大家尝试一下。
当然我这里想给大家介绍的是我在火狐也做了一个 Aurora 主题。对的!用的是同一套的颜色体系。喜欢的小伙伴可以试一下哦!
博主开始在B站直播学习,欢迎过来《直播间》一起学习。
我们在这里互相监督,互相鼓励,互相努力走上人生学习之路,让学习改变我们生活!
学习的路上,很枯燥,很寂寞,但是希望这样可以给我们彼此带来多一点陪伴,多一点鼓励。我们一起加油吧! (๑ •̀ㅂ•́)و
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!