#每日一記#防止按鈕在短時間內重複點選

羅小黑寫寫文字發表於2018-03-29

每日一記 - 但並不日更

很多時候我們點選按鈕來提交資料,但是在網路條件不好或者互動提示不明確的情況下,使用者會在段時間內多次點選按鈕,如果沒有對按鈕做保護就會造成重複的資料提交,造成資料異常,今天就分享一個比較通用的解決方案。

解決這個問題的思路就是增加一個變數來維護現有按鈕的狀態,但是一個頁面裡如果有很多按鈕,那麼就要申明同等數量的變數,這對維護來說很不友好。

##現有問題

<div class="button">
  提交
</div>
複製程式碼
var button = document.querySelector('.button');

button.onclick = submit;

function submit (e) {
  // 模擬非同步
  var promiseCb = new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve('提交成功');
    }, 1000);
  })
  
  return promiseCb.then(
    function (res) {
      // 處理回撥
      console.log(res);
    }
  )
}
複製程式碼

沒有提交中的保護

解決方法

為了更好的封裝舊的程式碼,就必須避免增加額外的變數,所以寫了一個 actionDelegate 函式來對原有的 action 進行封裝,而原有的程式碼只需要修改一下就可以了

button.onclick = submit;
↓
button.onclick = actionDelegate(submit);

function actionDelegate (action) {
  // do something
}
複製程式碼

然後我們要對 action 進行型別的判斷,如果 action 返回的是普通的物件,那麼我們認為這個 action 是一個同步的函式;如果 action 返回的是一個 promsie,那麼我們認為我們需要等待這個 promise 狀態結束後才能讓這個 action 再次執行

function actionDelegate (action) {
  // 獲取函式返回值
  var returnValue = action(e);

  // 判斷返回值是 promise
  if (returnValue && returnValue.constructor && 
    returnValue.constructor.name === 'Promise') {
      // 保護按鈕不被狂點
  }
  else {
    // let it go
  }
}
複製程式碼

所以在 submit 函式中最終必須要返回一個 promise 就變得很重要

function submit (e) {
  // 模擬非同步
  var promiseCb = new Promise(...)
  
  return promiseCb
}
複製程式碼

接著我們就要處理最重要的部分了,那就是儲存按鈕的狀態,在這個案例裡我通過 event 獲取到了按鈕的 node,並且把狀態儲存在 node 的 attr 上,這邊也可以使用其他的方式去儲存狀態

function actionDelegate (action) {
  return function (e) {
    if (e.target.getAttribute('progress-status') === 'processing') {
      // 如果按鈕上有處理中的狀態則跳過後續邏輯
      return false;
    }
    
    // 獲取函式返回值
    var returnValue = action(e);

    // 判斷返回值是 promise
    if (returnValue && returnValue.constructor && 
        returnValue.constructor.name === 'Promise') {
      
      // 關鍵點 把按鈕狀態儲存在 node 的屬性上 
      e.target.setAttribute('progress-status', 'processing')
    
      return returnValue.then(
        function () {
          // promise 結束後重置狀態
          e.target.setAttribute('progress-status', 'initial');
        }
      )
    }
  }
}
複製程式碼

最後程式碼組裝起來就是下面的樣子

var button = document.querySelector('.button');

button.onclick = actionDelegate(submit);

function submit (e) {
  // 模擬非同步
  var promiseCb = new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve('提交成功');
    }, 1000);
  })
  
  return promiseCb.then(
    function (res) {
      // 處理回撥
      console.log(res);
    }
  )
}

function actionDelegate (action) {
  return function (e) {
    if (e.target.getAttribute('progress-status') === 'processing') {
      // 如果按鈕上有處理中的狀態則跳過後續邏輯
      return false;
    }
    
    // 獲取函式返回值
    var returnValue = action(e);

    // 判斷返回值是 promise
    if (returnValue && returnValue.constructor && 
        returnValue.constructor.name === 'Promise') {

      var originInnerHTML = e.target.innerHTML;
      
      // 關鍵點 把按鈕狀態儲存在 node 的屬性上 
      e.target.setAttribute('progress-status', 'processing')
      e.target.innerHTML = '提交中...';
    
      return returnValue.then(
        function () {
          // promise 結束後重置狀態
          e.target.setAttribute('progress-status', 'initial');
          e.target.innerHTML = originInnerHTML;
        }
      )
    }
  }
}
複製程式碼

執行中則忽略點選

後記

本來是用 angularjs 實現的一個指令,用來代替 ng-click 的,後來發現在別的專案裡也要用,所以就用原生的程式碼重新實現了邏輯。在 angularjs 中可能會更好實現,因為指令本身會有獨立的作用域就不需要重複申明變數了。今天在寫教程的時候突然發現用 attr 來實現可能更方便,各位如果有更好的實現方式可以來交流。

謝謝

如果喜歡這篇文章 可以關注專欄 也請點贊分享哦

##JSbin

demo 原始碼

相關文章