解放你对按钮防抖处理的生产力

1,440 阅读2分钟

一、业务需求问题描述

  • 简单场景: 按钮提交请求一个服务端接口,接口长时间未响应,按钮重复提交不允许再次请求服务端接口
  • 复杂场景:上述场景只是一个很简单场景描述,有时候一个按钮提交处理逻辑有涉及页面许多表单输入控件的校验,还有处理dialog的显示(确定,取消逻辑)以及处理请求多个服务端接口的逻辑,按钮重复提交不允许再次请求服务端接口,甚至不允许点击页面中的其他按钮操作,这种场景一般在业务中经常遇到,那么该如何去正确处理按钮重复点击带来的问题

二、普通的解决方案

大部分人的做法都会先声明一个变量(如submiting),然后根据该boolean变量去决定是否去请求服务端接口,如下示例代码:

import axios from 'axios'

// 是否正在提交中
let submiting = false

/**
 * 表单校验
 * @return {Boolean}
 */
function validateForm() {
  // TODO
}

/**
 * 提交按钮逻辑监听器
 * 注意:submitListener是一个async异步函数,返回值默认是一个promise,该promise状态必须是fulfilled或者rejected,而不是pending状态
 */
async function submitListener() {
  if (submiting) {
    return
  }
  submiting = true

  if (!validateForm()) {
    submiting = false
    return
  }

  await axios.post('/api/xxx')
    .then(res => {
      // TODO
    })
    .catch(err => {
      // TODO
    })

  await axios.post('/api/yyy')
    .then(res => {
      // TODO
    })
    .catch(err => {
      // TODO
    })

  submiting = false
}

上述解决方案没有问题,缺点是如果一个页面有多个这样的逻辑,甚至一个项目有n处这样的逻辑需要处理,那么就需要我们去解放自己的生产力了

三、正确的解决方案

1 vue解决方案

1.1 提供vue的一个原型方法$lockListener

// lockListener.js
/* eslint-disable no-param-reassign */
/**
 * lock 处理按钮重复点击
 * @param {Function} handler 按钮click事件监听器,handler返回值一定需要是一个promise(且不能是pending状态)
 * @param {Number} delay 按钮连续点击之间延时多少毫秒执行代码,handler代码中有异常(包含Promise.reject()抛出的异常)不生效
 * @param {Function} lockValueChangeListener 按钮click事件监听器锁定值change事件监听器
 */

export default function lock(
  handler,
  delay = 1000,
  lockValueChangeListener = () => {},
) {
  // @思考1:为什么加一个lockValueChangeListener回调函数参数
  let callback = () => {};
  if (typeof lockValueChangeListener === 'function') {
    callback = (bl) => lockValueChangeListener(bl);
  }

  return async (...args) => {
    if (handler.$lock) {
      return;
    }

    handler.$lock = true;
    callback(true);

    // @思考2:下面代码为什么不这样处理`await handler.apply(this, args).catch(err => Project.reject(err))`
    // 还要赋值一个promise,然后promise.catch...
    const promise = handler.apply(this, args);
    promise.catch(() => {
      handler.$lock = false;
      callback(false);
    });

    await promise;
    await new Promise((resolve) => setTimeout(resolve, delay));

    handler.$lock = false;
    callback(false);
  };
}
// main.js app入口文件
import Vue from 'vue'
import lockListener from './lockListener.js'

Vue.prototype.$lockListener = lockListener

1.2 vue解决方案示例

备注:v-loading 是element-ui框架提供的指令

下面示例提交按钮连续点击之间的时间间隔最小是2秒,submitListener才会执行,如果submitListener执行耗时3秒,那么再次重复提交submitListener会立即执行

<template>
  <div class="xxx-page">
    <div
      element-loading-text="提交中,请稍后..."
      v-loading.fullscreen.lock="submitLocking"
      @click="(() => $lockListener(submitListener, 2000, locking => submitLocking = locking))()"
    >提交</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 提交锁定中(提交中)
      submitLocking: false,
    }
  },
  methods: {
    async submitListener() {
      if (!this.validateForm()) {
        return Promise.reject()
      }

      await axios.post('/api/xxx')
        .then(res => {
          // TODO
        })
        .catch(err => {
          // TODO
        })

      await axios.post('/api/yyy')
        .then(res => {
          // TODO
        })
        .catch(err => {
          // TODO
        })
    }
  }
}
</script>

1.3 上述代码留下的思考

  • 为什么加一个lockValueChangeListener回调函数参数

主要是监听锁定值的变化,从vue解决方案示例中可以看到,我们可以根据submitLocking来处理外部逻辑,如:页面显示是否提交中loading;如果正在提交中页面其他是否可以被点击之类的逻辑

  • 为什么不这样处理await handler.apply(this, args).catch(err => Project.reject(err))

原因是如果代码有异常,浏览器控制台点击错误信息不能直观的找到真正出错的代码行,不好定位具体哪块代码出现错误

2 react解决方案

2.1、提供一个装饰器函数

// lock.js
/**
 * @param {Number} delay 按钮连续点击之间延时多少毫秒执行代码,handler代码中有异常(包含Promise.reject()抛出的异常)不生效
 * @param {Function} lockValueChangeListener 按钮click事件监听器锁定值change事件监听器
 */
export default function lock(
  delay = 1000,
  lockValueChangeListener = () => {},
) {
  /**
   * @param {Object} target 装饰目标(类的原型对象)
   * @param {String} name 装饰目标成员(方法)
   * @param {Object} descripter 类原型对象的方法属性描述符
   */
  return function(target, name, descripter) {
    const handler = descripter.value

    let callback = () => {};
    if (typeof lockValueChangeListener === 'function') {
      callback = (bl) => lockValueChangeListener(bl);
    }

    descripter.value = async function() {
      if (handler.$lock) {
        return
      }

      handler.$lock = true
      callback(true)

      const promise = handler.apply(this, arguments)
      promise.catch(() => {
        handler.$lock = false
        callback(false)
      })

      await promise
      await new Promise(resolve => setTimeout(resolve, delay))

      handler.$lock = false
      callback(false)
    }
  
    return descripter
  }
}

2.2、react解决方案示例

import React from 'react'
import lock from './lock'

class Page extends React.Component {
  @lock(2000)
  async submitListener() {
    if (!this.validateForm()) {
      return Promise.reject()
    }

    await axios.post('/api/xxx')
      .then(res => {
        // TODO
      })
      .catch(err => {
        // TODO
      })

    await axios.post('/api/yyy')
      .then(res => {
        // TODO
      })
      .catch(err => {
        // TODO
      })
  }

  render() {
    return (
      <div class="xxx-page">
        <div onClick={ this.submitListener.bind(this) }>提交</div>
      </div>
    )
  }
}