Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 3854|回复: 0

Javascript事件机制兼容性解决方案

[复制链接]
  • TA的每日心情
    奋斗
    2024-4-6 11:05
  • 签到天数: 748 天

    [LV.9]以坛为家II

    2034

    主题

    2092

    帖子

    70万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    705612
    发表于 2021-4-5 13:29:31 | 显示全部楼层 |阅读模式

      本文的解决方案可以用于Javascript native对象和宿主对象(dom元素),通过以下的方式来绑定和触发事件:

      

     

      或者

    var input = document.getElementsByTagName('input')[0];
    var form = document.getElementsByTagName('form')[0];
    Evt.on(input, 'click', function(evt){
        console.log('input click1');
        console.log(evt.target === input);
        console.log(evt.modified);
        //evt.stopPropagation();
        console.log(evt.modified);
    });
    var handle2 = Evt.on(input, 'click', function(evt){
        console.log('input click2');
        console.log(evt.target === input);
        console.log(evt.modified);
    });
    Evt.on(form, 'click', function(evt){
        console.log('form click');
        console.log(evt.currentTarget === input);
        console.log(evt.target === input);
        console.log(evt.currentTarget === form);
        console.log(evt.modified);
    });
    Evt.emit(input, 'click');
    Evt.emit(input, 'click', {bubbles: true});
    handle2.remove();
    Evt.emit(input, 'click');
    View Code

     

    After函数

      

      为native对象添加事件的过程主要在after函数中完成,这个函数主要做了以下几件事:

    1. 如果obj中已有响应函数,将其替换成dispatcher函数
    2. 使用链式结构,保证多次绑定事件函数的顺序执行
    3. 返回一个handle对象,调用remove方法可以去除本次事件绑定

      下图为after函数调用前后onlog函数的引用

        

     

          (调用前)

      

          (调用后)

      详细解释请看注释,希望读者能够跟着运行一遍

    var after = function(target, method, cb, originalArgs){
        var existing = target[method];
        var dispatcher = existing;
        if (!existing || existing.target !== target) {
            //如果target中没有method方法,则为他添加一个方法method方法
            //如果target已经拥有method方法,但target[method]中target不符合要求则将method方法他替换
            dispatcher = target[method] = function(){
                //由于js是此法作用域:通过阅读包括变量定义在内的数行源码就能知道变量的作用域。
                //局部变量在声明它的函数体内以及其所嵌套的函数内始终是有定义的
                //所以在这个函数中可以访问到dispatcher变量
                var results = null;
                var args = arguments;
                if (dispatcher.around) {//如果原先拥有method方法,先调用原始method方法
                    //此时this关键字指向target所以不用target
                    results = dispatcher.around.advice.apply(this, args);
                }
                
                if (dispatcher.after) {//如果存在after链则依次访问其中的advice方法
                    var _after = dispatcher.after;
                    while(_after && _after.advice) {
                        //如果需要原始参数则传入arguments否则使用上次执行结果作为参数
                        args = _after.originalArgs ? arguments : results;
                        results = _after.advice.apply(this, args);
                        _after = _after.next;
                    }
                }
            }
            
            if (existing) {
            //函数也是对象,也可以拥有属性跟方法
            //这里将原有的method方法放到dispatcher中
                dispatcher.around = {
                    advice: function(){
                        return existing.apply(target, arguments);
                    }
                }
            }
            dispatcher.target = target;
        }
        
        
        var signal = {
            originalArgs: originalArgs,//对于每个cb的参数是否使用最初的arguments
            advice: cb,
            remove: function() {
                if (!signal.advice) {
                    return;
                }
                //remove的本质是将cb从函数链中移除,删除所有指向他的链接
                var previous = signal.previous;
                var next = signal.next;
                if (!previous && !next) {
                    dispatcher.after = signal.advice = null;
                    dispatcher.target = null;
                    delete dispatcher.after;
                } else if (!next){
                    signal.advice = null;
                    previous.next = null;
                    signal.previous = null;
                } else if (!previous){
                    signal.advice = null;
                    dispatcher.after = next;
                    next.previous = null;
                    signal.next = null;
                } else {
                    signal.advice = null;
                    previous.next = next;
                    next.previous = previous;
                    signal.previous = null;
                    signal.next = null;
                }
            }
        }
        
        
        var previous = dispatcher.after;
        if (previous) {//将signal加入到链式结构中,处理指针关系
            while(previous && previous.next && (previous = previous.next)){};
            previous.next = signal;
            signal.previous = previous;
        } else {//如果是第一次使用调用after方法,则dispatcher的after属性指向signal
            dispatcher.after = signal;
        }
        
        cb = null;//防止内存泄露
        return signal;
    }
    View Code

      

    解决兼容性

      IE浏览器从IE9开始已经支持DOM2事件处理程序,但是对于老版本的ie浏览器,任然使用attachEvent方式来为dom元素添加事件。值得庆幸的是微软已宣布2016年将不再对ie8进行维护,对于广大前端开发者无疑是一个福音。然而在曙光来临之前,仍然需要对那些不支持DOM2级事件处理程序的浏览器进行兼容性处理,通常需要处理以下几点:

    1. 多次绑定一个事件,事件处理函数的调用顺序问题
    2. 事件处理函数中的this关键字指向问题
    3. 标准化event事件对象,支持常用的事件属性

     

      由于使用attachEvent方法添加事件处理函数无法保证事件处理函数的调用顺序,所以我们弃用attachEvent,转而用上文中的after生成的正序链式结构来解决这个问题。

    //1、统一事件触发顺序
        function fixAttach(target, type, listener) {
        debugger;
            var listener = fixListener(listener);
            var method = 'on' + type;
            return after(target, method, listener, true);
        };
    View Code

     对于事件处理函数中的this关键字指向,通过闭包即可解决(出处),如:

      

       本文也是通过这种方式解决此问题

    //1、统一事件触发顺序
        function fixAttach(target, type, listener) {
        debugger;
            var listener = fixListener(listener);
            var method = 'on' + type;
            return after(target, method, listener, true);
        };
        
        function fixListener(listener) {
            return function(evt){
                //每次调用listenser之前都会调用fixEvent
                debugger;
                var e = _fixEvent(evt, this);//this作为currentTarget
                if (e && e.cancelBubble && (e.currentTarget !== e.target)){
                    return;
                }
                var results =  listener.call(this, e);
    
                if (e && e.modified) {
                    // 在整个函数链执行完成后将lastEvent回归到原始状态,
                    //利用异步队列,在主程序执行完后再执行事件队列中的程序代码
                    //常规的做法是在emit中判断lastEvent并设为null
                    //这充分体现了js异步编程的优势,把变量赋值跟清除代码放在一起,避免逻辑分散,缺点是不符合程序员正常思维方式
                    if(!lastEvent){
                        setTimeout(function(){
                            lastEvent = null;
                        });
                    }
                    lastEvent = e;
                }
                return results;
            }
        }
    View Code

      对于事件对象的标准化,我们需要将ie提供给我们的现有属性转化为标准的事件属性。

    function _fixEvent(evt, sender){
            if (!evt) {
                evt = window.event;
            }
            if (!evt) { // emit没有传递事件参数,或者通过input.onclick方式调用
                return evt;
            }
            if(lastEvent && lastEvent.type && evt.type == lastEvent.type){
            //使用一个全局对象来保证在冒泡过程中访问的是同一个event对象
            //chrome中整个事件处理过程event是唯一的
                evt = lastEvent;
            }
            var fixEvent = evt;
            // bubbles 和cancelable根据每次emit时手动传入参数设置
            fixEvent.bubbles = typeof evt.bubbles !== 'undefined' ? evt.bubbles : false;
            fixEvent.cancelable = typeof evt.cancelable !== 'undefined' ? evt.cancelable : true;
            fixEvent.currentTarget = sender;
            if (!fixEvent.target){ // 多次绑定统一事件,只fix一次
                fixEvent.target = fixEvent.srcElement || sender;
                
                fixEvent.eventPhase = fixEvent.target === sender ? 2 : 3;
                if (!fixEvent.preventDefault) {
                    fixEvent.preventDefault = _preventDefault;
                    fixEvent.stopPropagation = _stopPropagation;
                    fixEvent.stopImmediatePropagation = _stopImmediatePropagation;
                }
                //参考:http://www.nowamagic.net/javascript/js_EventMechanismInDetail.php
                if( fixEvent.pageX == null && fixEvent.clientX != null ) {
                    var doc = document.documentElement, body = document.body;
                    fixEvent.pageX = fixEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
                    fixEvent.pageY = fixEvent.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) - (doc && doc.clientTop  || body && body.clientTop  || 0);
                }
                if (!fixEvent.relatedTarget && fixEvent.fromEvent) {
                    fixEvent.relatedTarget = fixEvent.fromEvent === fixEvent.target ? fixEvent.toElement : fixEvent.fromElement;
                }
                // 参考: http://www.cnblogs.com/hsapphire/archive/2009/12/18/1627047.html
                if (!fixEvent.which && fixEvent.keyCode) {
                    fixEvent.which = fixEvent.keyCode;
                }
            }
            
            return fixEvent;
        }
        
        function _preventDefault(){
            this.defaultPrevented = true;
            this.returnValue = false;
    
            this.modified = true;
        }
        
        function _stopPropagation(){
            this.cancelBubble = true;
    
            this.modified = true;
        }
        
        function _stopImmediatePropagation(){
            this.isStopImmediatePropagation = true;
            this.modified = true;
        }
    View Code

      _preventDefault_stopPropagation_stopImmediatePropagation三个函数中我们,如果被调用则listener执行完后使用一个变量保存event对象(见fixListener),以便后序事件处理程序根据event对象属性进行下一步处理。stopImmediatePropagation函数,对于这个函数的模拟,我们同样通过闭包来解决。

      

      

      注意这里不能直接写成这种形式,上文中fixListener也是同样道理。

      

      需要注意一点,我们将event标准化目的还有一点,可以在emit方法中设置参数来控制事件过程,比如:

      Evt.emit(input, 'click');//不冒泡

      Evt.emit(input, 'click', {bubbles: true});//冒泡

      根据我的测试使用fireEvent方式触发事件,无法设置{bubbles:false}来阻止冒泡,所以这里我们用Javascript来模拟冒泡过程。同时在这个过程中也要保证event对象的唯一性。

    // 模拟冒泡事件
        var sythenticBubble = function(target, type, evt){
            var method = 'on' + type;
            var args = Array.prototype.slice.call(arguments, 2);
            // 保证使用emit触发dom事件时,event的有效性
            if ('parentNode' in target) {
                var newEvent = args[0] = {};
                for (var p in evt) {
                    newEvent[p] = evt[p];
                }
                
                newEvent.preventDefault = _preventDefault;
                newEvent.stopPropagation = _stopPropagation;
                newEvent.stopImmediatePropagation = _stopImmediatePropagation;
                newEvent.target = target;
                newEvent.type = type;
            }
            
            do{
                if (target && target[method]) {
                    target[method].apply(target, args);
                }
            }while(target && (target = target.parentNode) && target[method] && newEvent && newEvent.bubbles);
        }
        
        var emit = function(target, type, evt){
            if (target.dispatchEvent && document.createEvent){
                var newEvent = document.createEvent('HTMLEvents');
                newEvent.initEvent(type, evt && !!evt.bubbles, evt && !!evt.cancelable);
                if (evt) {
                    for (var p in evt){
                        if (!(p in newEvent)){
                            newEvent[p] = evt[p];
                        }
                    }
                }
                
                target.dispatchEvent(newEvent);
            } /*else if (target.fireEvent) {
                target.fireEvent('on' + type);// 使用fireEvent在evt参数中设置bubbles:false无效,所以弃用
            } */else {
                return sythenticBubble.apply(on, arguments);
            }
        }
    View Code

      附上完整代码:

    <!DOCTYPE html>
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
    <meta http-equiv="window-target" content="_top">
    <title>Writing to Same Doc</title>
    <script language="JavaScript">
    var after = function(target, method, cb, originalArgs){
        var existing = target[method];
        var dispatcher = existing;
        if (!existing || existing.target !== target) {
            //如果target中没有method方法,则为他添加一个方法method方法
            //如果target已经拥有method方法,但target[method]中target不符合要求则将method方法他替换
            dispatcher = target[method] = function(){
                //由于js是此法作用域:通过阅读包括变量定义在内的数行源码就能知道变量的作用域。
                //局部变量在声明它的函数体内以及其所嵌套的函数内始终是有定义的
                //所以在这个函数中可以访问到dispatcher变量
                var results = null;
                var args = arguments;
                if (dispatcher.around) {//如果原先拥有method方法,先调用原始method方法
                    //此时this关键字指向target所以不用target
                    results = dispatcher.around.advice.apply(this, args);
                }
                
                if (dispatcher.after) {//如果存在after链则依次访问其中的advice方法
                    var _after = dispatcher.after;
                    while(_after && _after.advice) {
                        //如果需要原始参数则传入arguments否则使用上次执行结果作为参数
                        args = _after.originalArgs ? arguments : results;
                        results = _after.advice.apply(this, args);
                        _after = _after.next;
                    }
                }
            }
            
            if (existing) {
            //函数也是对象,也可以拥有属性跟方法
            //这里将原有的method方法放到dispatcher中
                dispatcher.around = {
                    advice: function(){
                        return existing.apply(target, arguments);
                    }
                }
            }
            dispatcher.target = target;
        }
        
        
        var signal = {
            originalArgs: originalArgs,//对于每个cb的参数是否使用最初的arguments
            advice: cb,
            remove: function() {
                if (!signal.advice) {
                    return;
                }
                //remove的本质是将cb从函数链中移除,删除所有指向他的链接
                var previous = signal.previous;
                var next = signal.next;
                if (!previous && !next) {
                    dispatcher.after = signal.advice = null;
                    dispatcher.target = null;
                    delete dispatcher.after;
                } else if (!next){
                    signal.advice = null;
                    previous.next = null;
                    signal.previous = null;
                } else if (!previous){
                    signal.advice = null;
                    dispatcher.after = next;
                    next.previous = null;
                    signal.next = null;
                } else {
                    signal.advice = null;
                    previous.next = next;
                    next.previous = previous;
                    signal.previous = null;
                    signal.next = null;
                }
            }
        }
        
        
        var previous = dispatcher.after;
        if (previous) {//将signal加入到链式结构中,处理指针关系
            while(previous && previous.next && (previous = previous.next)){};
            previous.next = signal;
            signal.previous = previous;
        } else {//如果是第一次使用调用after方法,则dispatcher的after属性指向signal
            dispatcher.after = signal;
        }
        
        cb = null;//防止内存泄露
        return signal;
    }
    
    //1、统一事件触发顺序
    //2、标准化事件对象
    //3、模拟冒泡 emit时保持冒泡行为,注意input.onclick这种方式是不冒泡的
    //4、保持冒泡过程中event的唯一性
    
    window.Evt = (function(){
        var on = function(target, type, listener){
        debugger;
            if (!listener){
                return;
            }
            // 处理stopImmediatePropagation,通过包装listener来支持stopImmediatePropagation
            if (!(window.Event && window.Event.prototype && window.Event.prototype.stopImmediatePropagation)) {
                listener = _addStopImmediate(listener);
            }
        
            if (target.addEventListener) {
                target.addEventListener(type, listener, false);
                
                return {
                    remove: function(){
                        target.removeEventListener(type, listener);
                    }
                }
            } else {
                return fixAttach(target, type, listener);
            }
        };
        var lastEvent; // 使用全局变量来保证一个元素的多个listenser中事件对象的一致性,冒泡过程中事件对象的一致性;在chrome这些过程中使用的是同一个event
        //1、统一事件触发顺序
        function fixAttach(target, type, listener) {
        debugger;
            var listener = fixListener(listener);
            var method = 'on' + type;
            return after(target, method, listener, true);
        };
        
        function fixListener(listener) {
            return function(evt){
                //每次调用listenser之前都会调用fixEvent
                debugger;
                var e = _fixEvent(evt, this);//this作为currentTarget
                if (e && e.cancelBubble && (e.currentTarget !== e.target)){
                    return;
                }
                var results =  listener.call(this, e);
    
                if (e && e.modified) {
                    // 在整个函数链执行完成后将lastEvent回归到原始状态,
                    //利用异步队列,在主程序执行完后再执行事件队列中的程序代码
                    //常规的做法是在emit中判断lastEvent并设为null
                    //这充分体现了js异步编程的优势,把变量赋值跟清除代码放在一起,避免逻辑分散,缺点是不符合程序员正常思维方式
                    if(!lastEvent){
                        setTimeout(function(){
                            lastEvent = null;
                        });
                    }
                    lastEvent = e;
                }
                return results;
            }
        }
        
        function _fixEvent(evt, sender){
            if (!evt) {
                evt = window.event;
            }
            if (!evt) { // emit没有传递事件参数,或者通过input.onclick方式调用
                return evt;
            }
            if(lastEvent && lastEvent.type && evt.type == lastEvent.type){
            //使用一个全局对象来保证在冒泡过程中访问的是同一个event对象
            //chrome中整个事件处理过程event是唯一的
                evt = lastEvent;
            }
            var fixEvent = evt;
            // bubbles 和cancelable根据每次emit时手动传入参数设置
            fixEvent.bubbles = typeof evt.bubbles !== 'undefined' ? evt.bubbles : false;
            fixEvent.cancelable = typeof evt.cancelable !== 'undefined' ? evt.cancelable : true;
            fixEvent.currentTarget = sender;
            if (!fixEvent.target){ // 多次绑定统一事件,只fix一次
                fixEvent.target = fixEvent.srcElement || sender;
                
                fixEvent.eventPhase = fixEvent.target === sender ? 2 : 3;
                if (!fixEvent.preventDefault) {
                    fixEvent.preventDefault = _preventDefault;
                    fixEvent.stopPropagation = _stopPropagation;
                    fixEvent.stopImmediatePropagation = _stopImmediatePropagation;
                }
                //参考:http://www.nowamagic.net/javascript/js_EventMechanismInDetail.php
                if( fixEvent.pageX == null && fixEvent.clientX != null ) {
                    var doc = document.documentElement, body = document.body;
                    fixEvent.pageX = fixEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
                    fixEvent.pageY = fixEvent.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) - (doc && doc.clientTop  || body && body.clientTop  || 0);
                }
                if (!fixEvent.relatedTarget && fixEvent.fromEvent) {
                    fixEvent.relatedTarget = fixEvent.fromEvent === fixEvent.target ? fixEvent.toElement : fixEvent.fromElement;
                }
                // 参考: http://www.cnblogs.com/hsapphire/archive/2009/12/18/1627047.html
                if (!fixEvent.which && fixEvent.keyCode) {
                    fixEvent.which = fixEvent.keyCode;
                }
            }
            
            return fixEvent;
        }
        
        function _preventDefault(){
            this.defaultPrevented = true;
            this.returnValue = false;
    
            this.modified = true;
        }
        
        function _stopPropagation(){
            this.cancelBubble = true;
    
            this.modified = true;
        }
        
        function _stopImmediatePropagation(){
            this.isStopImmediatePropagation = true;
            this.modified = true;
        }
        
        function _addStopImmediate(listener) {
            return function(evt) { // 除了包装listener外,还要保证所有的事件函数共用一个evt对象
                if (!evt.isStopImmediatePropagation) {
                    //evt.stopImmediatePropagation = _stopImmediateProgation;
                    return listener.apply(this, arguments);
                }
            }
        }
        
        // 模拟冒泡事件
        var sythenticBubble = function(target, type, evt){
            var method = 'on' + type;
            var args = Array.prototype.slice.call(arguments, 2);
            // 保证使用emit触发dom事件时,event的有效性
            if ('parentNode' in target) {
                var newEvent = args[0] = {};
                for (var p in evt) {
                    newEvent[p] = evt[p];
                }
                
                newEvent.preventDefault = _preventDefault;
                newEvent.stopPropagation = _stopPropagation;
                newEvent.stopImmediatePropagation = _stopImmediatePropagation;
                newEvent.target = target;
                newEvent.type = type;
            }
            
            do{
                if (target && target[method]) {
                    target[method].apply(target, args);
                }
            }while(target && (target = target.parentNode) && target[method] && newEvent && newEvent.bubbles);
        }
        
        var emit = function(target, type, evt){
            if (target.dispatchEvent && document.createEvent){
                var newEvent = document.createEvent('HTMLEvents');
                newEvent.initEvent(type, evt && !!evt.bubbles, evt && !!evt.cancelable);
                if (evt) {
                    for (var p in evt){
                        if (!(p in newEvent)){
                            newEvent[p] = evt[p];
                        }
                    }
                }
                
                target.dispatchEvent(newEvent);
            } /*else if (target.fireEvent) {
                target.fireEvent('on' + type);// 使用fireEvent在evt参数中设置bubbles:false无效,所以弃用
            } */else {
                return sythenticBubble.apply(on, arguments);
            }
        }
        
        return {
            on: on,
            emit: emit
        };
    })()
    </script>
    <style type="text/css"></style>
    </head>
    <body>
      <form>
        <input type="button" value="Replace Content" >
      </form>
    </body>
    </html>
    View Code

      脑图

      欢迎各位有志之士前来交流探讨!

    参考文章:

    【前端盲点】事件的几个阶段你真的了解么???

    JavaScript事件机制详细研究 

    [解惑]JavaScript事件机制

     javascript线程解释(setTimeout,setInterval你不知道的事)

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-3 10:40 , Processed in 0.082468 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表