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

类中带事件与参数的removeEventListener失效问题求助

问题描述

我正在构建一个处理Symfony表单AJAX请求与响应的类,通过MutationObserver监听DOM变化,为新增表单重新绑定事件监听器。为避免同一表单绑定多个重复事件,需要移除除最后一个外的所有监听器,但removeEventListener始终无法生效。

已知addEventListener和removeEventListener必须使用同一个函数实例,问题应该出在sendAsyncForm返回的匿名函数上——每次调用sendAsyncForm都会生成新的函数,导致移除时找不到匹配的监听器。但我需要同时传递事件对象和自定义参数,尝试多种方式都没解决,特此求助。


HTML代码

<table class="table" id="admin__table">
    <thead>
        <tr>
            <th>Id</th>
            <th>Nombre</th>
            <th>Email</th>
            <th>Roles</th>
            <th>CreationDate</th>
            <th></th>
            <th></th>
            <th></th>
        </tr>
    </thead>
    <tbody>
    {% for admin in admins %}
        <tr {% if not admin.isActive %}style="background-color: #ffaaaa;"{% endif %}>
            <td>{{ admin.id }}</td>
            <td>
                {% set showPath = path('app_admin_show', {'id': admin.id}) %}
                <div class="text-primary" data-mdb-toggle="modal" data-mdb-target="#crudModal" type="button" onclick="asyncHTML('{{ showPath }}', 'crud-modal__body')">
                    {{ admin.name }} {{ admin.firstname }}
                </div>
            </td>
            <td>{{ admin.email }}</td>
            <td>{{ admin.roles ? admin.roles|json_encode : '' }}</td>
            <td>{{ admin.creationDate ? admin.creationDate|date('Y-m-d H:i:s') : '' }}</td>
            <td>
                <div data-mdb-toggle="modal" data-mdb-target="#crudModal">
                    {% include "components/admin/triggers/admin_edit-button.html.twig" with {'path': path('app_admin_edit', {'id': admin.id}), 'HTMLPlaceDOMelementId': 'crud-modal__body'} %}
                </div>
            </td>
            <td>{% include "admin/_block_form.html.twig" %}</td>
            <td>{% include "admin/_delete_form.html.twig" %}</td>
        </tr>
    {% else %}
        <tr>
            <td colspan="10">no records found</td>
        </tr>
    {% endfor %}
    </tbody>
</table>

<script>
    blockFormObserver();

    function blockFormObserver() {
        let observer = new MutationObserver(function(mutations, observer) {
            let forms = document.querySelectorAll("form[async][name='admin__block']");
            let asyncFormArray = [];

            for (let i = 0; i < forms.length; i++) {
                asyncFormArray[i] = new AsyncForm(forms[i]);

                asyncFormArray[i].setResponse(function() {response => console.log("response")});
                asyncFormArray[i].setOnsuccess(() => asyncHTML("{{ path('app_admin_index') }}", "admin__index"));

                asyncFormArray[i].prepare();
                if (i < forms.length-1) asyncFormArray[i].remove();
            }
        });

        observer.observe(document, {
            subtree: true,
            attributes: true,
        });
    }
</script>

JavaScript代码

class AsyncForm {
    constructor(form) {
        if (form == null) throw new AsyncFormException('Form passed to constructor is null', this.form);
        if (form.getAttribute("action") == null) throw new AsyncFormException('Action attribute wasn`t found', this.form);

        this.form = form;
        this.action = this.form.getAttribute("action");
        this.previous = false;
        this.response = false;
        this.onsuccess = false;
    }

    prepare() {
        this.form.addEventListener("submit", this.sendAsyncForm(this.response, this.onsuccess));
    }

    remove() {
        this.form.removeEventListener("submit", this.sendAsyncForm(this.response, this.onsuccess));
    }

    sendAsyncForm(response, onsuccess) {
        return (e) => {
            e.preventDefault();
            console.log(response);
            var formData = new FormData(this.form);
            $.ajax({
                url : this.action,
                type : 'POST',
                data : formData,
                cache: false,
                processData: false,
                contentType: false,
                dataType : 'html',
                success : response => {
                    if (this.onsuccess) this.onsuccess();
                },
                error : (xhr, status) => {
                    if (xhr.status = 422) this.response(xhr.responseText);
                    else response("<div class='form-error'>Ocurrió un problema al enviar el formulario, disculpe las molestias.</div>");
                },
            });
        }
    }

    setAction(action) {
        this.action = action;
    }

    setPrevious(previous) {
        this.previous = previous;
    }

    setResponse(response) {
        this.response = response;
    }

    setOnsuccess(onsuccess) {
        this.onsuccess = onsuccess;
    }
}

function AsyncFormException(message, form) {
    this.message = message;
    this.form = form;
    this.name = "AsyncFormException";
}

解决方案

核心问题是每次调用sendAsyncForm都会生成全新的函数实例,导致addEventListener和removeEventListener引用的不是同一个函数。解决思路是把生成的回调函数存储在AsyncForm实例中,确保添加和移除时用同一个引用。

修改后的AsyncForm类

class AsyncForm {
    constructor(form) {
        if (form == null) throw new AsyncFormException('Form passed to constructor is null', this.form);
        if (form.getAttribute("action") == null) throw new AsyncFormException('Action attribute wasn`t found', this.form);

        this.form = form;
        this.action = this.form.getAttribute("action");
        this.previous = false;
        this.response = false;
        this.onsuccess = false;
        this.submitHandler = null; // 存储回调函数引用
    }

    prepare() {
        // 先移除已存在的监听器(防止重复绑定)
        if (this.submitHandler) {
            this.form.removeEventListener("submit", this.submitHandler);
        }
        // 生成并存储回调函数
        this.submitHandler = (e) => {
            e.preventDefault();
            console.log(this.response);
            var formData = new FormData(this.form);
            $.ajax({
                url : this.action,
                type : 'POST',
                data : formData,
                cache: false,
                processData: false,
                contentType: false,
                dataType : 'html',
                success : () => {
                    if (this.onsuccess) this.onsuccess();
                },
                error : (xhr, status) => {
                    if (xhr.status === 422) { // 修复赋值为比较的错误
                        this.response(xhr.responseText);
                    } else {
                        this.response("<div class='form-error'>Ocurrió un problema al enviar el formulario, disculpe las molestias.</div>");
                    }
                },
            });
        };
        this.form.addEventListener("submit", this.submitHandler);
    }

    remove() {
        if (this.submitHandler) {
            this.form.removeEventListener("submit", this.submitHandler);
            this.submitHandler = null; // 清空引用
        }
    }

    // 其他方法保持不变
    setAction(action) {
        this.action = action;
    }

    setPrevious(previous) {
        this.previous = previous;
    }

    setResponse(response) {
        this.response = response;
    }

    setOnsuccess(onsuccess) {
        this.onsuccess = onsuccess;
    }
}

function AsyncFormException(message, form) {
    this.message = message;
    this.form = form;
    this.name = "AsyncFormException";
}

MutationObserver逻辑优化

原代码中每次DOM变化都会遍历所有表单并尝试移除前N-1个的监听器,可简化为直接为每个表单创建实例时先解绑再绑定,避免重复绑定:

function blockFormObserver() {
    let observer = new MutationObserver(function(mutations, observer) {
        let forms = document.querySelectorAll("form[async][name='admin__block']");
        
        forms.forEach(form => {
            // 检查表单是否已绑定过AsyncForm实例,避免重复创建
            if (!form._asyncFormInstance) {
                form._asyncFormInstance = new AsyncForm(form);
                form._asyncFormInstance.setResponse(response => console.log("response"));
                form._asyncFormInstance.setOnsuccess(() => asyncHTML("{{ path('app_admin_index') }}", "admin__index"));
            }
            // 先移除再重新绑定,确保只有一个监听器生效
            form._asyncFormInstance.remove();
            form._asyncFormInstance.prepare();
        });
    });

    observer.observe(document, {
        subtree: true,
        attributes: true,
        childList: true // 新增监听DOM节点增删,更准确捕获表单新增
    });
}

关键修改说明

  1. 存储回调引用:通过submitHandler属性保存回调函数,确保add和remove使用同一个函数实例。
  2. 移除冗余方法:删除sendAsyncForm,直接在prepare中定义回调,避免每次调用生成新函数。
  3. 修复AJAX错误:把xhr.status = 422的赋值操作改为===比较。
  4. 避免重复实例化:给表单标记_asyncFormInstance属性,减少不必要的对象创建。
  5. 优化Observer配置:添加childList: true,更精准监听表单的新增操作。

内容的提问来源于stack exchange,提问作者Diego García

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 06:35:26