1. 遇到的问题
- 问题一:接受一个老项目,现在会将这个项目以 H5 页面嵌入到 app 中。遇到的问题就是在同一个页面获取定位调用了很多次,但是现在 app 的规范对于获取用户定位信息的次数受到限制,一个页面只能获取一次,不能多次调用 app 的 api 获取用户定位。然后看了一下项目的代码,获取定位的方法,进行了统一的封装,但是存在在不同的组件内进行调用,这样就导致同一个页面,调用了多次定位方法。
- 问题二:订单预览界面接口较慢,出现了用户在接口没有响应的情况下,多次提交了订单,造成同一订单,进行了多次下单。
2. 解决办法
-
问题一解决办法:
1.1 将组件内部的获取定位的方法全部提到页面,页面获取定位后传到组件内部,再用页面的定位进行组件内的操作或请求;
1.2 使用状态管理,比如 redux ,在组件内部监听状态的改变,也就是定位值的获取,然后进行组件的下步操作;
1.3 使用 useContext 获取页面的定位状态,当然这种方法其实和方法二的状态管理是一个,都是监听页面获取定位的值,然后进行组件内部操作;
1.4 上边的方法修改代码量都比较大,遇到的问题主要需求就是:不管页面调用了多少次定位方法,调用 app 的 api 只能是一次。又不想大量修改原来的代码,经过分析,改造调用定位的方法,使用发布-订阅模式。 -
问题二解决办法:
2.1 其实这个问题是开发者在开发中最高频率出现的问题,解决的办法也很多,比如在页面声明一个变量,判断变量是否提交,修改变量的值来阻止多次提交;
2.2 使用遮罩层,提交后显示遮罩层,禁止用户操作,提交响应完成,关闭遮罩层;
2.3 按钮的切换,提交后切换到加载或者置灰按钮,该按钮不能进行操作,提交响应完成,按钮再切换回来;
2.4 在发布-订阅模式里边添加一个存储事件,获取事件是否存在,删除事件的方法,当然主要是为了解决第一个问题,所以直接将第二个问题的解决办法也合并一起解决。
3. 基础方法实现
- 存储事件变量;
- 查询事件是否已经存在;
- 初始化事件存储;
- 删除当前事件;
- 清除存储空间中的全部事件;
- 监听事件,将事件完成后需要触发的回调进行存储保存;
- 事件触发,获取事件的全部回调,并循环执行回调,回调完成,删除当前事件。
4. 事件存储
- 一般都是采用 Object 对象进行存储,为什么采用 Map 实现呢?
- 方便查询,删除,清空等操作。
export class EventEmitter{
constructor(){
this.event = new Map();
}
}
5. 查询事件是否存在
- 直接调用 Map 的 has 方法,查询事件是否存在。
export class EventEmitter{
constructor(){
this.event = new Map();
}
has(type){
return this.event.has(type)
}
}
6. 初始化事件回调列表
- 判断事件是否存在,存在不做处理;
- 不存在就添加事件并初始化收集回调的数组。
export class EventEmitter{
constructor(){
this.event = new Map();
}
has(type){
return this.event.has(type)
}
init(type){
if(!this.event.has(type)){
this.event.set(type, [])
}
}
}
7. 删除当前事件
- 调用 Map 的 delete 方法,删除当前事件。
export class EventEmitter{
constructor(){
this.event = new Map();
}
has(type){
return this.event.has(type)
}
init(type){
if(!this.event.has(type)){
this.event.set(type, [])
}
}
delete(type){
this.event.delete(type)
}
}
8. 清空事件列表
- 调用 Map 的 clear 方法,清空所有事件。
export class EventEmitter{
constructor(){
this.event = new Map();
}
has(type){
return this.event.has(type)
}
init(type){
if(!this.event.has(type)){
this.event.set(type, [])
}
}
delete(type){
this.event.delete(type)
}
clear(){
this.event.clear()
}
}
9. 添加回调
- 判断事件是否存在;
- 如果不存在,就设置当前事件,并存储回调函数;
- 如果存在,就将回调函数存入回调函数列表中。
export class EventEmitter{
constructor(){
this.event = new Map();
}
has(type){
return this.event.has(type)
}
init(type){
if(!this.event.has(type)){
this.event.set(type, [])
}
}
delete(type){
this.event.delete(type)
}
clear(){
this.event.clear()
}
on(type, resolve, reject){
if(!this.event.has(type)){
this.event.set(type, [{resolve, reject}])
} else {
this.event.get(type).push({resolve, reject})
}
}
}
10. 触发事件的回调
- 判断事件是否存在;
- 存在就获取回调列表;
- 循环触发回调;
- 回调列表触发完成,删除当前事件。
export class EventEmitter{
constructor(){
this.event = new Map();
}
has(type){
return this.event.has(type)
}
init(type){
if(!this.event.has(type)){
this.event.set(type, [])
}
}
delete(type){
this.event.delete(type)
}
clear(){
this.event.clear()
}
on(type, resolve, reject){
if(!this.event.has(type)){
this.event.set(type, [{resolve, reject}])
} else {
this.event.get(type).push({resolve, reject})
}
}
emit(type, data, ansType){
if(this.event.has(type)){
this.event.get(type).forEach(item => {
if(ansType ==='resolve'){
item.resolve(data)
} else if(ansType ==='reject'){
item.reject(data)
}
})
this.delete(type)
}
}
}
11. 使用 EventEmitter
11.1 异步请求
const ev = new EventEmitter()
return new Promise((resolve, reject) => {
// 生成一个唯一的事件名
let eventName = generateReqkey({
url,
method,
data
})
// 判断是否请求已存在,存在则订阅存在的请求返回,不存在就添加一个初始值,发起请求
if(ev.has(eventName)){
ev.on(eventName, resolve, reject)
return false;
} else {
ev.init(eventName)
}
Taro.request({
...,
success(res){
// 判断code状态,成功调用 resolve,失败调用 reject
ev.emit(eventName, res, 'resolve')
resolve(res)
},
fail(err){
ev.emit(eventName, err, 'reject')
reject(err)
}
})
})
11.2 防止重复点击
const ev = new EventEmitter()
// 点击事件
function handle(){
if(ev.has('SUBMIT_ORDER_INFO')){
// 提示用户已下单操作
return false;
}
// 第一次进入,不存在提交事件,初始化一个提交事件
ev.init('SUBMIT_ORDER_INFO')
// 模拟提交异步
setTimeout(() => {
// 异步操作完成,清空本次事件,可以用户再次点击
ev.delete('SUBMIT_ORDER_INFO')
},5000)
}
12. 总结
- 注意在请求订阅发布中,我把第一次的请求并没有放入订阅中,因此在请求成功或者失败的时候,需要单独调用 resolve,reject。
- 针对上边的问题,我想到的解决办法是单独申明一个 eventNames 的存储空间,记录事件名,不记录回调事件,两个分开存储。
- const ev = new EventEmitter() 可以和状态管理一样,声明一个全局事件存储空间,其他页面引入就好,这样有个优势就是在其他异步防重复请求,和防重复点击事件时,使用全局的事件存储,不用每个页面都进行申明。
- 防重复点击这样的优势就是不用重复的每个页面申明变量,只用记录每个防重复的事件名就可以解决。
- 此对象还可以解决滚动加载,在异步没返回前,不进行下一次的滚动加载。
13. 根据总结的问题改造
export class EventEmitter{
constructor(){
this.event = new Map();
this.eventNames = new Set();
}
has(type){
return this.eventNames.has(type)
}
init(type){
if(!this.eventNames.has(type)){
this.eventNames.add(type)
}
}
delete(type){
this.event.delete(type)
this.eventNames.delete(type)
}
clear(){
this.event.clear()
this.eventNames.clear()
}
on(type, resolve, reject){
if(!this.event.has(type)){
this.event.set(type, [{resolve, reject}])
} else {
this.event.get(type).push({resolve, reject})
}
}
emit(type, data, ansType){
if(this.event.has(type)){
this.event.get(type).forEach(item => {
if(ansType ==='resolve'){
item.resolve(data)
} else if(ansType ==='reject'){
item.reject(data)
}
})
this.delete(type)
}
}
}
区别在将事件回调和事件名分别使用 Map 和 Set 进行存储,这样判断事件是否存在,和订阅所有的请求,就互相不冲突,解决了第一次请求不进入发布-订阅的监听,返回回调,统一使用触发事件,进行发布。
14. 改造后的使用
14.1 防重复请求改造后的调用
const ev = new EventEmitter()
return new Promise((resolve, reject) => {
// 生成一个唯一的事件名
let eventName = generateReqkey({
url,
method,
data
})
// 判断是否请求已存在,存在则订阅存在的请求返回,不存在就添加一个初始值,发起请求
ev.on(eventName, resolve, reject)
if(ev.has(eventName)){
return false;
}
ev.init(eventName)
Taro.request({
...,
success(res){
// 判断code状态,成功调用 resolve,失败调用 reject
ev.emit(eventName, res, 'resolve')
},
fail(err){
ev.emit(eventName, err, 'reject')
}
})
})
防止重复点击还是按照原来的方法使用,不做改变。
15. 总结
- 代码实现很简单,只有四十行代码,但是解决了很多防止重复的问题;
- 同样的原理,使用的场景不同,产生的效果和结果就不同,能解决问题的办法多总结出现问题的共性,统一解决。