【taro react】 ---- 使用发布-订阅模式解决重复请求和重复点击问题

257 阅读7分钟

1. 遇到的问题

  1. 问题一:接受一个老项目,现在会将这个项目以 H5 页面嵌入到 app 中。遇到的问题就是在同一个页面获取定位调用了很多次,但是现在 app 的规范对于获取用户定位信息的次数受到限制,一个页面只能获取一次,不能多次调用 app 的 api 获取用户定位。然后看了一下项目的代码,获取定位的方法,进行了统一的封装,但是存在在不同的组件内进行调用,这样就导致同一个页面,调用了多次定位方法。
  2. 问题二:订单预览界面接口较慢,出现了用户在接口没有响应的情况下,多次提交了订单,造成同一订单,进行了多次下单。

2. 解决办法

  1. 问题一解决办法:
    1.1 将组件内部的获取定位的方法全部提到页面,页面获取定位后传到组件内部,再用页面的定位进行组件内的操作或请求;
    1.2 使用状态管理,比如 redux ,在组件内部监听状态的改变,也就是定位值的获取,然后进行组件的下步操作;
    1.3 使用 useContext 获取页面的定位状态,当然这种方法其实和方法二的状态管理是一个,都是监听页面获取定位的值,然后进行组件内部操作;
    1.4 上边的方法修改代码量都比较大,遇到的问题主要需求就是:不管页面调用了多少次定位方法,调用 app 的 api 只能是一次。又不想大量修改原来的代码,经过分析,改造调用定位的方法,使用发布-订阅模式。

  2. 问题二解决办法:
    2.1 其实这个问题是开发者在开发中最高频率出现的问题,解决的办法也很多,比如在页面声明一个变量,判断变量是否提交,修改变量的值来阻止多次提交;
    2.2 使用遮罩层,提交后显示遮罩层,禁止用户操作,提交响应完成,关闭遮罩层;
    2.3 按钮的切换,提交后切换到加载或者置灰按钮,该按钮不能进行操作,提交响应完成,按钮再切换回来;
    2.4 在发布-订阅模式里边添加一个存储事件,获取事件是否存在,删除事件的方法,当然主要是为了解决第一个问题,所以直接将第二个问题的解决办法也合并一起解决。

3. 基础方法实现

  1. 存储事件变量;
  2. 查询事件是否已经存在;
  3. 初始化事件存储;
  4. 删除当前事件;
  5. 清除存储空间中的全部事件;
  6. 监听事件,将事件完成后需要触发的回调进行存储保存;
  7. 事件触发,获取事件的全部回调,并循环执行回调,回调完成,删除当前事件。

4. 事件存储

  1. 一般都是采用 Object 对象进行存储,为什么采用 Map 实现呢?
  2. 方便查询,删除,清空等操作。
export class EventEmitter{
  constructor(){
    this.event = new Map();
  }
}

5. 查询事件是否存在

  1. 直接调用 Map 的 has 方法,查询事件是否存在。
export class EventEmitter{
  constructor(){
    this.event = new Map();
  }
  has(type){
    return this.event.has(type)
  }
}

6. 初始化事件回调列表

  1. 判断事件是否存在,存在不做处理;
  2. 不存在就添加事件并初始化收集回调的数组。
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. 删除当前事件

  1. 调用 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. 清空事件列表

  1. 调用 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. 添加回调

  1. 判断事件是否存在;
  2. 如果不存在,就设置当前事件,并存储回调函数;
  3. 如果存在,就将回调函数存入回调函数列表中。
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. 触发事件的回调

  1. 判断事件是否存在;
  2. 存在就获取回调列表;
  3. 循环触发回调;
  4. 回调列表触发完成,删除当前事件。
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. 总结

  1. 注意在请求订阅发布中,我把第一次的请求并没有放入订阅中,因此在请求成功或者失败的时候,需要单独调用 resolve,reject。
  2. 针对上边的问题,我想到的解决办法是单独申明一个 eventNames 的存储空间,记录事件名,不记录回调事件,两个分开存储。
  3. const ev = new EventEmitter() 可以和状态管理一样,声明一个全局事件存储空间,其他页面引入就好,这样有个优势就是在其他异步防重复请求,和防重复点击事件时,使用全局的事件存储,不用每个页面都进行申明。
  4. 防重复点击这样的优势就是不用重复的每个页面申明变量,只用记录每个防重复的事件名就可以解决。
  5. 此对象还可以解决滚动加载,在异步没返回前,不进行下一次的滚动加载。

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. 总结

  1. 代码实现很简单,只有四十行代码,但是解决了很多防止重复的问题;
  2. 同样的原理,使用的场景不同,产生的效果和结果就不同,能解决问题的办法多总结出现问题的共性,统一解决。