是否可以保证onchange将比添加到标记的事件更早运行?

时间:2018-05-12 22:56:59

标签: javascript

我正在从服务器端生成一个标签:

'<select class="form-data api command" data-command-name="GetDifficultyData" data-events="change" name="id" data-data="" onchange="this.attributes[\'data-data\']=\'data=\' + this.value">'.$difficultyOptions.'</select>'

我正在使用PHP这样做,但这与此问题无关。

但我有一个function会在我的event代码中添加select,因为它已被标记为处理command

function initializeCommands(root) {
    if (!root) root = document;
    if (!radWindow) {
        radWindow = document.getElementById("radwindow");
        var closeRadWindow = function(event) {
            radWindow.addClass("hide-popup");
        };
        radWindow.addEventListener("click", closeRadWindow);
        var popupActionContainer = radWindow.querySelector(".popup-action-container");
        popupActionContainer.addEventListener("click", function(event) {
            if (!event) {
                event = window.event;
            }
            event.cancelBubble = true;
            if (event.stopPropagation) {
                event.stopPropagation();
            }
            return false;            
        });
        popupActionContainer.addEventListener("keyup", function(e) {
            if ((e.code == 'Enter') && (e.target.tagName.toLowerCase() !== "textarea")) {
                var mainButton = this.querySelector("span.api[data-form-name]");
                mainButton ? mainButton.click() : closeRadWindow();
            }
        });
        document.getElementsByTagName("body")[0].addEventListener("keyup", function(e) {
            if (e.code == 'Escape') {
                closeRadWindow();
            }
        });
        popupActionContainer.querySelector(".icon-close").addEventListener("click", closeRadWindow);
    }
    if (!popupContent) {
        popupContent = radWindow.querySelector("#popup-content");
    }
    var commandButtons = root.querySelectorAll(".api:not(.initialized)");
    for (var commandIndex = 0; commandIndex < commandButtons.length; commandIndex++) {
        var eventsToHandle = commandButtons[commandIndex].attributes["data-events"].value.split(" ");
        for (var eventIndex = 0; eventIndex < eventsToHandle.length; eventIndex++) {
            commandButtons[commandIndex].addEventListener(eventsToHandle[eventIndex], function(event) {
                if (this.hasClass("confirm")) {
                    if (!confirm(this.hasAttribute("data-confirm-message") ? this.attributes["data-confirm-message"].value : "Biztos ebben?")) {
                        return;
                    }
                }
                if (this.hasClass("template")) {
                    var data = this.attributes["data-data"];
                    sendRequest("POST", "/template/" + this.attributes["data-template-name"].value, templateCallback, true, (data ? data.value : data));
                } else if (this.hasClass("form")) {
                    var container = document.querySelector(this.attributes["data-container"].value);
                    var items = container.querySelectorAll(".form-data");
                    var data = [];
                    var props = [];
                    for (var index = 0; index < items.length; index++) {
                        var v = items[index].value;
                        if (items[index].tagName.toLowerCase() === "input") {
                            if (items[index].type === "checkbox") {
                                v = items[index].checked;
                            }
                        }
                        data.push(items[index].name + "=" + v);
                        props.push(items[index].name);
                    }
                    var recaptchaResponse = container.querySelectorAll("[name=g-recaptcha-response]");
                    if (recaptchaResponse.length > 0) {
                        data.push("g-recaptcha-response=" + recaptchaResponse[0].value);
                        props.push("g-recaptcha-response");
                    }
                    var errorPlaces = container.querySelectorAll('.error-text:not(.invisible)');
                    for (var errorIndex = 0; errorIndex < errorPlaces.length; errorIndex++) {
                        errorPlaces[errorIndex].addClass("invisible");
                    }
                    var validationResults = validate(items);
                    if (validationResults.length) {
                        for (var errorIndex = 0; errorIndex < validationResults.length; errorIndex++) {
                            var errorPlace = container.querySelector('.error-text[data-key="' + validationResults[errorIndex].key + '"]');
                            if (errorPlace) {
                                errorPlace.removeClass("invisible").innerText = validationResults[errorIndex].value;
                            }
                        }
                    } else {
                        sendRequest("POST", "/form/" + this.attributes["data-form-name"].value, function() {
                            if (this.readyState === 4) {
                                var r = JSON.parse(this.responseText);
                                for (var key in r) {
                                    if (r[key] && (props.indexOf(key) >= 0)) {
                                        var errorPlace = container.querySelector('.error-text[data-key="' + key + '"]');
                                        if (errorPlace) {
                                            errorPlace.removeClass("invisible").innerText = r[key];
                                        }
                                    }
                                }
                                eval(r.response);
                            }
                        }, true, data.join("&"));
                    }
                } else if (this.hasClass("command")) {
                    var data = this.attributes["data-data"];
                    sendRequest("POST", "/command/" + this.attributes["data-command-name"].value, function() {
                        if (this.readyState === 4) {
                            var r = JSON.parse(this.responseText);
                            if (r.callback) {
                                eval(r.callback);
                            }
                        }
                    }, true, ((data && data.value) ? data.value : data));
                }
            });
        }
        commandButtons[commandIndex].addClass("initialized");
    }
}

function在页面加载时运行,并且在某些情况下通过API生成弹出窗口内容(我目前的情况是弹出窗口)。总结一下情况:

  • 页面加载
  • 用户点击按钮
  • 将请求发送到服务器
  • 服务器响应弹出内容,包括我在此问题开头显示的标签,onchange在服务器端定义,因此从标签生命周期的开始就有效
  • 正在执行弹出回调,标签将包含change
  • 的事件处理程序

执行顺序的重要性在于{<1}}应该在事件之后执行以后添加到标记中,因为事件将发送onchange选择标签到服务器(因此它将询问有关要编辑的实体的数据),发送当前id非常重要。我当前的测试运行良好,id正在onchange事件之前执行,后来被添加到标记中,一切都很好。但是,我担心某些浏览器或未来版本可能不会以同样的方式发生。在这种情况下,我需要做一些进一步的工作,以保证首先执行change,然后才执行onchange事件。无论如何我可以做这项工作,但我想保持代码优雅,避免编写不必要的代码。所以,问题是:在我的情况下是否保证change将首先执行,然后onchange事件后来被定义为change事件?

修改

我们可以使用此原型控制事件的顺序:

onchange

例如,如果我们访问https://stackoverflow.com/

并在控制台中运行原型代码并将此原型用作:

function CustomEventHandler() {
    let events = new Map();
    this.add = (tags, eventKeys, eventFunctions) => {
        let isNew = false;
        for (let tag of tags) {
            if (!events.has(tag)) events.set(tag, new Map());
            let outerItem = events.get(tag);
            for (let eventKey of eventKeys) {
                if (isNew = !outerItem.has(eventKey)) outerItem.set(eventKey, []);
                let innerItem = outerItem.get(eventKey);
                for (let eventFunction of eventFunctions) innerItem.push(eventFunction);
                if (isNew) {
                    tag.addEventListener(eventKey, () => {this.execute(tag, eventKey)});
                }
            }
        }
        return this;
    };

    this.execute = (tag, eventKey) => {
        if (events.has(tag)) {
            let outerItem = events.get(tag);
            if (outerItem.has(eventKey)) {
                let innerItem = outerItem.get(eventKey);
                for (let func of innerItem) func(tag, eventKey);
            }
        }
    };

    this.get = () => {return events};
}

然后我们可以看到事件以正确的顺序正确执行。我们可以添加自定义功能,例如向函数添加标识符并能够通过标识符删除函数,或者交换它们的顺序,但为了简单起见,我省略了这些功能的实现。在我们的例子中,全局范围用于简化。

3 个答案:

答案 0 :(得分:5)

DOM Level 3(HTML5附带)现在按照附加顺序对事件进行排序。

以下是对规范的引用:

https://www.w3.org/TR/DOM-Level-3-Events/Overview.html#changes-DOMEvents2to3Changes-flow

  

<强> 11.1.1。对DOM Level 2事件流的更改

     

这个新规范引入了以下新概念   事件流程:

     
      
  • 现在订购了事件监听器。在DOM Level 2中,事件排序   没有具体说明。
  •   

答案 1 :(得分:2)

您的问题可以归结为:“通过HTML属性关联的事件顺序与通过DOM对象属性关联的同一事件随着时间的推移会保持一致吗?”

答案是最有可能的,但不能保证。如果需要确定性,请不要使用任何一种技术,请使用DOM .addEventListener()方法进行事件注册。就目前正在排序的事件而言,这是另一个答案实际上指的是什么。当你注册事件.addEventListener(),对于特定事件,你可以放心的是,回调将在他们注册的顺序被调用。它不适用于HTML属性或DOM对象属性关联的事件。

答案 2 :(得分:1)

是的,事件处理程序的顺序也得到了保证,但不是很直接。

规范have a note指出:

  

仅当将事件处理程序的值设置为非null且尚未激活事件处理程序时,才进行事件侦听器注册。由于侦听器是按照注册时的顺序调用的,因此假设没有发生停用,特定事件类型的事件侦听器的顺序将始终为:

     
      
  1. 在事件处理程序的值首次设置为非null之前,向addEventListener()注册的事件侦听器

  2.   
  3. 然后设置当前设置的回调(如果有)

  4.   
  5. ,最后在第一次将事件处理程序的值设置为非null之后,向addEventListener()注册了事件侦听器。

  6.   

这非常简洁,这意味着只有事件处理程序IDL(或属性解析)的第一个设置才能确定其顺序。后续的任何设置都将与第一次设置时具有相同的顺序,除非在两次设置之间将其禁用...

btn.addEventListener('click', e=>console.log('1'));
btn.onclick = e => console.log('2');
btn.addEventListener('click', e=>console.log('3'));

console.log('###test 1###');
btn.click(); //1, 2, 3

btn.addEventListener('click', e=>console.log('4'));
btn.onclick = e => console.log('should be 5 oO');
btn.addEventListener('click', e=>console.log('6'));

console.log('###test 2###');
btn.click(); //1, "should be 5 oO", 3, 4, 6

btn.addEventListener('click', e=>console.log('7'));
btn.onclick = null; // deactivate
btn.onclick = e => console.log('8 oO');
btn.addEventListener('click', e=>console.log('9'));

console.log('###test 4###');
btn.click(); //1, 3, 4, 6, 7, "8 oO", 9
<button id="btn">click me</button>

但是请注意,这只是规范的立足点,我们知道并非所有实现都始终遵循规范。