美文网首页
【JS 】给HTML元素增加OnCreated事件

【JS 】给HTML元素增加OnCreated事件

作者: 冰麟轻武 | 来源:发表于2023-03-22 20:22 被阅读0次

    本文档涉及知识点

    1. 如何监听页面dom变化
    2. 如何触发自定义事件
    3. 如何让自定义事件支持onxxx属性绑定
    4. 如何跳过原生js事件,仅触发jQuery冒泡
    5. 如何给还未创建的元素绑定事件
    6. 利用分割函数+微队列处理长时任务,防止页面假死

    1. 背景

    在维护老项目的时候经常会有这样的需求:为页面某个特定元素增加一些功能或操作

    但是,你可能无法确定页面元素会在什么时候以什么方式被添加页面上

    特别是在代码中存在各种动态创建元素的项目中

    部分页

    $.html(...)


    第三方组件

    所以必须有一种方法可以捕获元素创建行为

    2. MutationObserver

    MutationObserver 接口提供了监视对 DOM 树所做更改的能力。

    并且兼容性也没问题

    官方 demo

     // 选择需要观察变动的节点
    const targetNode = document.getElementById('some-id');
    
    // 观察器的配置(需要观察什么变动)
    const config = { attributes: true, childList: true, subtree: true };
    
    // 当观察到变动时执行的回调函数
    const callback = function(mutationsList, observer) {
        // Use traditional 'for loops' for IE 11
        for(let mutation of mutationsList) {
            if (mutation.type === 'childList') {
                console.log('A child node has been added or removed.');
            }
            else if (mutation.type === 'attributes') {
                console.log('The ' + mutation.attributeName + ' attribute was modified.');
            }
        }
    };
    
    // 创建一个观察器实例并传入回调函数
    const observer = new MutationObserver(callback);
    
    // 以上述配置开始观察目标节点
    observer.observe(targetNode, config);
    
    // 之后,可停止观察
    observer.disconnect();
    

    3. 让js支持 created 事件

    目标是以下两种方式都可以监听created事件

    window.addEventListener("created", function (e) {
        console.log("window.addEventListener >> ", e.target);
    }, true);
    
    $(document).on("created", "*", function (e) {
        console.log("$(document).on >> ",  e.target);
    })
    

    之前直接依赖 jQuery 实现过一个版本,但效率比较差,重新实现一个原生的提高性能,并兼容jQuery

    3.1 创建 MutationObserver

    // 等页面加载完成后再执行,
    window.addEventListener("load", function () {
        // 当观察到变动时执行的回调函数
        function callback(mutationsList) {
            const array = [];
            for (let i = 0; i < mutationsList.length; i++) {
                const nodes = mutationsList[i].addedNodes;
                for (var j = 0; j < nodes.length; j++) {
                    const node = nodes[j];
                    if ("tagName" in node) { // 忽略没有 tagName 的元素,如:#text 等
                        array.push(node);
                    }
                }
            }
            // 触发数组中所有元素的 created 事件
            if (array.length > 0) {
                oncreated(array);
            }
        };
    
        // 创建一个观察器实例并传入回调函数
        const observer = new MutationObserver(callback);
        observer.observe(document, { childList: true, subtree: true });
    
        // 将创建观察器之前的就已经创建元素全部处理一遍
        oncreated(document.children);
    });
    

    3.2 MutationObserver.takeRecords

    方法返回已检测到但尚未由观察者的回调函数处理的所有匹配 DOM 更改的列表,使变更队列保持为空。

    由于 MutationObserver.observe 是异步且回调时间不可控,有时候一个元素创建了3,4秒还不触发回调,会导致一些问题

    所以需要一个额外的定时轮询来加速处理

    setInterval(() => callback(observer.takeRecords()), 100);
    

    3.3 手动触发事件

    参考 创建和触发 events

    var event = new Event('build');// Listen for the event.
    elem.dispatchEvent(event);
    

    为每个元素原型中增加一个方法来触发 created 事件

    /**
     * 触发元素的created事件
     */
    Element.prototype.created = function() {
        const event = new Event('created');
        elem.dispatchEvent(event);
    }
    

    4. 执行 oncreated

    但是这个方式触发的事件有一个缺陷

    无法触发 属性中的 onXXXX 的代码

    这部分需要手动完成

    很明显,原生事件click可以正确被执行,但自定义事件created不行

    /**
     * 以事件方式执行指定代码
     * @param {Element} elem 需要触发事件的元素 
     * @param {Event} event 需要触发的事件参数
     * @param {String} code 需要触发的时间代码 
     */
    function dispatchByCode(elem, event, code) {
        try {
            new Function("event", code).apply(elem, [event]); // 修改 this 作用域,并传入参数 event
        } catch (e) {
            console.error(e);
        }
    }
    

    所以完整的触发事件代码改成这样 (eventType in elem.__proto__) 用于判断是否是原生属性,如onclick会返回true

    /**
     * 触发元素的created事件
     */
    Element.prototype.created = function() {
        const event = new Event('created');
        dispatch(this, event);
    }
    
    /**
     * 触发元素事件
     * @param {Element} elem 需要触发事件的元素 
     * @param {Event} event 需要触发的事件参数
     */
    function dispatch(elem, event) {
        const eventType = "on" + event.type;
        if (!(eventType in elem.__proto__)) {
            const code = elem.getAttribute(eventType);
            if (code) {
                dispatchByCode(elem, event, code);
            }
        }
        elem.dispatchEvent(event);
    }
    

    3.5 oncreated 函数

    现在就可以实现 3.1 中未完成的 oncreated函数了

    /**
     * 递归循环元素的子元素, 并在 hookeds 中返回钩子元素
     * @param {Array} elements 元素数组
     * @param {Set} [hookeds] 需要触发 oncreated 事件的元素集合
     */
    function oncreated(elements, hookeds) {
        if (hookeds == null) {
            hookeds = new Set();
            oncreated(elements, hookeds);
            hookeds = Array.from(hookeds);
            while (hookeds.length > 0) {
                const topN = hookeds.splice(0, 300);
                setTimeout(() => topN.forEach(x => x.created()), 50);
            }
        }
        for (let i = 0; i < elements.length; i++) {
            const element = elements[i];
            if (isHooking(element)) {
                hookeds.add(element);
            }
            if (element.childElementCount > 0) {
                oncreated(element.children, hookeds);
            }
        }
    }
    
    /**
     * 判断一个元素是否是钩子元素
     * @param {Element} element 待判断的元素 
     * @returns {boolean} 是/否
     */
    function isHooking(element) {
        return true; // 预留后期优化,页面元素不多的情况下直接返回true即可
    }
    
    const topN = hookeds.splice(0, 1000);
    setTimeout(() => topN.forEach(x => x.created()));
    

    使用微队列每300个元素执行一次,防止页面卡死

    到现在为止已经可以触发 window.addEventListenerOnCreated.js(1)

    但还没有办法触发 jQuery.on(...)

    3.6 触发 jQuery.on(...)

    但曾经地表最强的 jQuery 为我们提供了丰富的API

    只要使用 triggerHandler(type, [data]) + eve.isImmediatePropagationStopped() 就可以轻松的实现

    window.addEventListener("created", function (e) {
        if (window.$) {
            const $e = new window.$.Event(e);
            $e.isImmediatePropagationStopped = function () {
                return this.target !== this.currentTarget;
            }
            jQueryBubble(window.$, e.target, $e);
        }
    }, true);
    
    /**
     * 模拟jQuery中的事件冒泡
     * @param {jQuery} $ jQuery 主函数
     * @param {Element} elem 事件元素
     * @param {jQuery.Event} event 事件 
     */
    function jQueryBubble($, elem, event) {
        $(elem).triggerHandler(event);
        if (elem === document.documentElement) {
            jQueryBubble($, document, event);
        } else if (elem.parentElement) {
            jQueryBubble($, elem.parentElement, event);
        }
    }
    

    4. 完整代码

    (_ => {
        if (Element.prototype.created) {
            return;
        }
    
        /**
         * 触发元素的created事件
         */
        Element.prototype.created = function () {
            const event = new Event('created');
            dispatch(this, event);
        }
    
        // 计数器
        const counter = {
            count: 0, // 捕获元素个数
            hookeds: 0, // 触发元素个数
            last: 0, // 记录最后观察到的元素个数
            stopwatch: [0, 0], // 记录执行时间
            hookedStart() {
                let begin = new Date().getTime();
                return () => {
                    if (begin) {
                        this.stopwatch[1] += new Date().getTime() - begin;
                        begin = null;
                    }
                }
            },
            capturedStart() {
                let begin = new Date().getTime();
                return () => {
                    if (begin) {
                        this.stopwatch[0] += new Date().getTime() - begin;
                        begin = null;
                    }
                }
            }
        };
    
        const createdHooks = _.createdHooks = [];
    
        /**
         * 触发元素事件
         * @param {Element} elem 需要触发事件的元素 
         * @param {Event} event 需要触发的事件参数
         */
        function dispatch(elem, event) {
            if (!elem || !event || !event.type) {
                return;
            }
            const eventType = "on" + event.type;
            if (!(eventType in elem.__proto__)) {
                const code = elem.getAttribute(eventType);
                if (code) {
                    dispatchByCode(elem, event, code);
                }
            }
            elem.dispatchEvent(event);
        }
    
        /**
         * 以事件方式执行指定代码
         * @param {Element} elem 需要触发事件的元素 
         * @param {Event} event 需要触发的事件参数
         * @param {String} code 需要触发的时间代码 
         */
        function dispatchByCode(elem, event, code) {
            try {
                new Function("event", code).apply(elem, [event]);
            } catch (e) {
                console.error(e);
            }
        }
    
        /**
         * 判断一个元素是否是钩子元素
         * @param {Element} element 待判断的元素 
         * @returns {boolean} 是/否
         */
        function isHooking(element) {
            if (element === document.documentElement || element === document || element === document.body || element === document.head) {
                return false;
            }
            if (createdHooks.length == 0) {
                return true;
            }
            for (let i = 0; i < createdHooks.length; i++) {
                const hook = createdHooks[i];
                const test = typeof hook.test === "function" ? hook.test : hookTest;
                if (test(element, hook)) {
                    return true;
                }
            }
            return false;
        }
    
        /**
         * 判断一个元素是否是钩子元素
         * @param {Element} element 待判断的元素
         * @param {Object} hook 钩子配置 
         * @returns {boolean} 是/否
         */
        function hookTest(element, hook) {
            if (hook.tagName && element.tagName !== hook.tagName) {
                return false;
            }
            if (hook.attr && !(hook.attr in element.attributes)) {
                return false;
            }
            return true;
        }
    
        /**
         * 递归循环元素的子元素, 并在 hookeds 中返回钩子元素
         * @param {Array} elements 元素数组
         * @param {Set} [hookeds] 需要触发 oncreated 事件的元素集合
         */
        function oncreated(elements, hookeds) {
            if (hookeds == null) {
                const stop = counter.capturedStart();
                hookeds = new Set();
                oncreated(elements, hookeds);
                hookeds = Array.from(hookeds);
                counter.hookeds += hookeds.length;
                stop();
                while (hookeds.length > 0) {
                    const topN = hookeds.splice(0, 300);
                    setTimeout(() => {
                        const stop = counter.hookedStart();
                        topN.forEach(x => x.created(), 50);
                        stop();
                    });
                }
                return;
            }
    
            counter.count += elements.length;
            for (let i = 0; i < elements.length; i++) {
                const element = elements[i];
                if (isHooking(element)) {
                    hookeds.add(element);
                }
                if (element.childElementCount > 0) {
                    oncreated(element.children, hookeds);
                }
            }
        }
    
        // 当观察到变动时执行的回调函数
        function callback(mutationsList) {
            const stop = counter.capturedStart();
            const array = [];
            for (let i = 0; i < mutationsList.length; i++) {
                const nodes = mutationsList[i].addedNodes;
                for (var j = 0; j < nodes.length; j++) {
                    const node = nodes[j];
                    if ("tagName" in node) {
                        array.push(node);
                    }
                }
            }
            stop();
            if (array.length > 0) {
                oncreated(array);
            }
        };
    
        // 防抖函数
        function debounce(seconds, fn) {
            let timer = null;
            return function (then) {
                timer && clearTimeout(timer);
                timer = setTimeout(() => {
                    fn();
                    then();
                }, seconds * 1000);
            }
        }
    
        const timeoutSecond = 5;
        const over = debounce(timeoutSecond, () => {
            const duration = (counter.stopwatch[0] + counter.stopwatch[1]) / 1000;
            const info = {
                页面元素: (document.querySelectorAll("*").length - 3) + " 个",
                捕获元素: counter.count + " 个",
                触发元素: counter.hookeds + " 个",
                捕获执行时间: (counter.stopwatch[0] / 1000).toFixed(3) + " 秒",
                触发执行时间: (counter.stopwatch[1] / 1000).toFixed(3) + " 秒",
            };
            if (duration > 5) {
                console.group("OnCreated执行时间过长")
            } else {
                console.group("OnCreated执行完成")
            }
            console.table(info)
            console.groupEnd();
        });
    
        // 页面加载事件
        window.addEventListener("load", function () {
            counter.begin = new Date().getTime(); // 开始时间
    
            // 创建一个观察器实例并传入回调函数
            const observer = new MutationObserver(callback);
            observer.observe(document, { childList: true, subtree: true });
    
            // MutationObserver 是异步的,回调时间不可控,所以需要设置一个轮询
            setInterval(() => callback(observer.takeRecords()), 100);
    
            oncreated(document.children);
            const watcher = setInterval(() => {
                const diff = counter.count - counter.last;
                if (diff < 0) {
                    return;
                }
                counter.last = counter.count
                if (diff > 100) {
                    over(() => this.clearInterval(watcher));
                }
    
            }, 1000);
            over(() => this.clearInterval(watcher));
        })
    
        // 兼容 JQuery
        if (_.window) {
            _.window.addEventListener("created", function (e) {
                if (_.jQuery) {
                    const $e = new $.Event(e);
                    $e.isImmediatePropagationStopped = function () {
                        return this.target !== this.currentTarget;
                    }
                    jQueryBubble(_.jQuery, e.target, $e);  // 如果这样代码对页面造成了性能影响 可以考虑注释掉
                    _.jQuery(document).triggerHandler($e);
                }
            }, true);
    
            /**
             * 模拟jQuery中的事件冒泡
             * @param {jQuery} $ 
             * @param {Element} elem 
             * @param {jQuery.Event} event 
             */
            function jQueryBubble($, elem, event) {
                while (elem) {
                    $(elem).triggerHandler(event);
                    elem = elem.parentElement;
                }
            }
        }
    
    })(window);
    

    5. 使用场景

    例1. 将QueryString自动填入同名input

    $(document.body).on("created", ":text[name]", function (e) {
        const elem = e.target;
        if (elem.name) {
            const reg = new RegExp("(^|&)" + elem.name.trim() + "=([^&]*)(&|$)");
            const r = location.search.substr(1).match(reg);
            if (r) {
                elem.value = decodeURI(r[2]);
            }
        }
    })
    
    image.png

    例2. 各种控件的自动创建

    $(document.body).on("created", "input[type=date]", function (e) {
        const elem = e.target;
        elem.type = "text"
        $(elem).datetimepicker({
            format: 'yyyy-mm-dd',
            minView: 2,
            autoclose: true,
            todayBtn: true,
        });
    })
    

    例3. 处理 style[scoped]

    【JS 】让浏览器支持<style scoped> - 简书 (jianshu.com)

    window.addEventListener("created", e => {
        if(e.target.tagName === "STYLE")){
            updateStyle(e.target)
        }
    }, true);
    

    6. 已知问题



    捕获阶段的效率没有问题,但触发OnCreated事件触发的比较多的情况下,效率就需要看回调方法的执行时间了
    另外模拟触发jQuery冒泡在元素很多的页面也会有一定的性能问题,如果不需要兼容jQuery可以去掉
    也可以修改为

    window.addEventListener("created", function (e) {
        if (window.jQuery) {
            const $e = new windiw.jQuery.Event(e);
            $e.isImmediatePropagationStopped = function () {
                return this.target !== this.currentTarget;
            }
            // 仅触发 $(document).on("created", "...", ...) ,2w个元素可以从3.5s下降到0.2s
            window.jQuery(document).triggerHandler($e);
        }
    }, true);

    相关文章

      网友评论

          本文标题:【JS 】给HTML元素增加OnCreated事件

          本文链接:https://www.haomeiwen.com/subject/xqnuldtx.html