You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何清除按钮旧事件监听器,避免触发重复提交事件

问题描述

我有一个交易数据表,每行都配有一个Edit按钮,点击后可编辑数据并保存至数据库。当前遇到的问题:若依次点击多个行的Edit按钮,后续保存时所有被点击过的行都会使用最后一次编辑的数据。例如点击前5行的Edit按钮后,在最后点击的行中将数量修改为5,保存后所有此前点击过的行的数量都会变为5。

我曾尝试移除saveBtn和Edit按钮的事件监听器,但并未奏效。问题根源应该是事件监听器仍在后台运行——所有saveBtn和Edit按钮使用了相同的类名,点击一个Edit按钮后再点击另一个,会产生多个监听器实例。请问当用户点击不同的Edit按钮时,如何删除saveBtn的事件监听器?

相关代码
const csvToTable = (csv, transactionIdsAtIndex) => {
    let rows = csv.trim().split(/\r?\n|\r/);
    let table = '';
    let tableRows = '';
    let tableHeader = '';
    rows.forEach((row, rowIndex) => {
        let tableColumns = '';
        let columns = row.split(','); // split/separate the columns in a row
        if (rowIndex === 0) {
            columns.unshift('');
        } else {
            columns.unshift('<button class="btn edit">Edit</button>');
        }
        columns.forEach((column) => {
            tableColumns += rowIndex == 0 ? '<th>' + column + '</th>' : '<td>' + column + '</td>';
        });
        if (rowIndex == 0) {
            tableHeader += `<tr id="${rowIndex}">` + tableColumns + '</tr>';
        } else {
            tableRows += `<tr id="${rowIndex}">` + tableColumns + '</tr>';
        }
    });
    table += '<table>';
    table += '<thead>';
        table += tableHeader;
    table += '</thead>';
    table += '<tbody>';
        table += tableRows;
    table += '</tbody>';
    table += '</table>';
    if (csvTable.classList.contains('hidden')) {
        csvTable.classList.remove('hidden');
    }
    csvTable.innerHTML += table;
    downloadBtn.classList.remove('hidden');
    handleEvents(transactionIdsAtIndex);
}

const handleEvents = (transactionIdsAtIndex) => {
    const editBtn = document.querySelectorAll('.edit');
    editBtn.forEach(btn => btn.addEventListener('click', function editTransaction() {
        editTransactionForm(transactionIdsAtIndex[btn.parentElement.parentElement.id]);
        btn.removeEventListener('click', editTransaction);
    }));
}

const editTransactionForm = (id) => {
    console.log('editing')
    editForm.classList.remove('hidden');
    downloadMenu.classList.add('hidden');
    downloadBtn.classList.add('hidden');

    api.call('Get', {typeName: 'FuelTransaction', search: {id: id}}, result => {
        const transaction = result[0];
        volume.value = (transaction.volume / 3.785412534257983).toFixed(2);
        cost.value = transaction.cost.toFixed(2);
        pricePerUnit.value = (cost.value / volume.value).toFixed(2);
        fuelType.value = transaction.productType;

        saveBtn.addEventListener('click', function validateFormData () {
            console.log('saving');
            let valid = true;
            if (isNaN(volume.value) || isNaN(cost.value) || isNaN(pricePerUnit.value)) {
                valid = false;
                popupText.textContent = 'Input must be a number';
                popup.classList.remove('hidden');
            } else if (!volume.value || !cost.value || !pricePerUnit.value) {
                valid = false;
                popupText.textContent = 'Missing input';
                popup.classList.remove('hidden');
            } else if (fuelType.value.length === 0) {
                valid = false;
                popupText.textContent = 'Must enter a fuel type';
                popup.classList.remove('hidden');
            }
            if (valid) {
                transaction.volume = Number(volume.value * 3.785412534257983);
                transaction.cost = Number(cost.value);
                transaction.productType = String(fuelType.value);
                popupText.textContent = 'Saving...';
                popup.classList.remove('hidden');
                api.call('Set', {typeName: 'FuelTransaction', entity: transaction},
                () => {
                    saveBtn.removeEventListener('click', validateFormData);
                    popup.classList.add('hidden');
                    editForm.classList.add('hidden');
                    downloadMenu.classList.remove('hidden');
                    getTransactionDetails();
                },
                err => {
                    popupText.textContent = 'Error saving transaction';
                    popup.classList.remove('hidden');
                    console.log(err)
                })
            }
        })
        
    })
}
解决方案

方法一:用全局变量跟踪并移除旧监听器

每次打开新的编辑表单前,先移除saveBtn上绑定的旧监听器,确保只有当前编辑对应的监听器生效:

// 全局变量保存当前的保存监听器
let currentSaveListener = null;

const editTransactionForm = (id) => {
    // 移除之前绑定的旧监听器
    if (currentSaveListener) {
        saveBtn.removeEventListener('click', currentSaveListener);
        currentSaveListener = null;
    }

    console.log('editing')
    editForm.classList.remove('hidden');
    downloadMenu.classList.add('hidden');
    downloadBtn.classList.add('hidden');

    api.call('Get', {typeName: 'FuelTransaction', search: {id: id}}, result => {
        const transaction = result[0];
        volume.value = (transaction.volume / 3.785412534257983).toFixed(2);
        cost.value = transaction.cost.toFixed(2);
        pricePerUnit.value = (cost.value / volume.value).toFixed(2);
        fuelType.value = transaction.productType;

        // 保存当前监听器到全局变量
        currentSaveListener = function validateFormData () {
            console.log('saving');
            let valid = true;
            if (isNaN(volume.value) || isNaN(cost.value) || isNaN(pricePerUnit.value)) {
                valid = false;
                popupText.textContent = 'Input must be a number';
                popup.classList.remove('hidden');
            } else if (!volume.value || !cost.value || !pricePerUnit.value) {
                valid = false;
                popupText.textContent = 'Missing input';
                popup.classList.remove('hidden');
            } else if (fuelType.value.length === 0) {
                valid = false;
                popupText.textContent = 'Must enter a fuel type';
                popup.classList.remove('hidden');
            }
            if (valid) {
                transaction.volume = Number(volume.value * 3.785412534257983);
                transaction.cost = Number(cost.value);
                transaction.productType = String(fuelType.value);
                popupText.textContent = 'Saving...';
                popup.classList.remove('hidden');
                api.call('Set', {typeName: 'FuelTransaction', entity: transaction},
                () => {
                    saveBtn.removeEventListener('click', currentSaveListener);
                    currentSaveListener = null;
                    popup.classList.add('hidden');
                    editForm.classList.add('hidden');
                    downloadMenu.classList.remove('hidden');
                    getTransactionDetails();
                },
                err => {
                    popupText.textContent = 'Error saving transaction';
                    popup.classList.remove('hidden');
                    console.log(err)
                })
            }
        }
        saveBtn.addEventListener('click', currentSaveListener);
    })
}

方法二:只绑定一次保存监听器,跟踪当前编辑实例

初始化时给saveBtn只绑定一次监听器,用全局变量记录当前正在编辑的交易,避免多次绑定监听器:

// 全局变量记录当前正在编辑的交易
let currentEditingTransaction = null;

// 页面初始化时绑定一次保存监听器
saveBtn.addEventListener('click', function validateFormData () {
    if (!currentEditingTransaction) return;

    console.log('saving');
    let valid = true;
    if (isNaN(volume.value) || isNaN(cost.value) || isNaN(pricePerUnit.value)) {
        valid = false;
        popupText.textContent = 'Input must be a number';
        popup.classList.remove('hidden');
    } else if (!volume.value || !cost.value || !pricePerUnit.value) {
        valid = false;
        popupText.textContent = 'Missing input';
        popup.classList.remove('hidden');
    } else if (fuelType.value.length === 0) {
        valid = false;
        popupText.textContent = 'Must enter a fuel type';
        popup.classList.remove('hidden');
    }
    if (valid) {
        currentEditingTransaction.volume = Number(volume.value * 3.785412534257983);
        currentEditingTransaction.cost = Number(cost.value);
        currentEditingTransaction.productType = String(fuelType.value);
        popupText.textContent = 'Saving...';
        popup.classList.remove('hidden');
        api.call('Set', {typeName: 'FuelTransaction', entity: currentEditingTransaction},
        () => {
            popup.classList.add('hidden');
            editForm.classList.add('hidden');
            downloadMenu.classList.remove('hidden');
            currentEditingTransaction = null;
            getTransactionDetails();
        },
        err => {
            popupText.textContent = 'Error saving transaction';
            popup.classList.remove('hidden');
            console.log(err)
        })
    }
});

const editTransactionForm = (id) => {
    console.log('editing')
    editForm.classList.remove('hidden');
    downloadMenu.classList.add('hidden');
    downloadBtn.classList.add('hidden');

    api.call('Get', {typeName: 'FuelTransaction', search: {id: id}}, result => {
        // 更新当前编辑的交易实例
        currentEditingTransaction = result[0];
        volume.value = (currentEditingTransaction.volume / 3.785412534257983).toFixed(2);
        cost.value = currentEditingTransaction.cost.toFixed(2);
        pricePerUnit.value = (cost.value / volume.value).toFixed(2);
        fuelType.value = currentEditingTransaction.productType;
    })
}

方法三:克隆按钮清除所有监听器

每次打开编辑表单时,克隆saveBtn替换原元素,克隆后的元素会自动清除所有绑定的事件监听器:

const editTransactionForm = (id) => {
    // 克隆saveBtn替换原元素,清除所有旧监听器
    const newSaveBtn = saveBtn.cloneNode(true);
    saveBtn.parentNode.replaceChild(newSaveBtn, saveBtn);
    saveBtn = newSaveBtn;

    console.log('editing')
    editForm.classList.remove('hidden');
    downloadMenu.classList.add('hidden');
    downloadBtn.classList.add('hidden');

    api.call('Get', {typeName: 'FuelTransaction', search: {id: id}}, result => {
        const transaction = result[0];
        volume.value = (transaction.volume / 3.785412534257983).toFixed(2);
        cost.value = transaction.cost.toFixed(2);
        pricePerUnit.value = (cost.value / volume.value).toFixed(2);
        fuelType.value = transaction.productType;

        saveBtn.addEventListener('click', function validateFormData () {
            // ... 原有的验证和保存逻辑
        })
    })
}

内容的提问来源于stack exchange,提问作者mrplayingame

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 15:02:03