浏览器事件

200 阅读8分钟

目录

  • 事件流
  • 事件处理程序
  • 事件对象
  • 内存与性能

事件历史回顾

  • 事件最早是在 IE3 和 Netscape Navigator 2 中出现的,目地是把某些表单处理工作从服务器转移到浏览器上来。
  • IE4 和 Netscape Navigator 3,这两家浏览器都提供了类似但又不同的API,而且持续了好几代。
  • DOM2 以符合逻辑的方式来标准化 DOM 事件 API。目前所有现代浏览器都实现了 DOM2 Events 的核心部分。
  • IE8 是最后一个使用专有事件系统的主流浏览器。

一、事件流:

事件流描述了页面接收事件的顺序。

1. IE 和 Netscape 开发团队提出了几乎完全相反的事件流方案:

  • IE 将支持事件冒泡流
  • Netscape Communicator 将支持事件捕获流。
  • 由于旧版本浏览器不支持,因此实际当中几乎不会使用事件捕获。通常使用事件冒泡。

2. 事件流详情

  • 事件冒泡:从最具体的元素向上传播...
  • 事件捕获:最不具体的节点 最先收到事件,最具体...后..
  • DOM事件流:DOM2 Events 规范规定事件流分为 3 个阶段:事件捕获、到达目标和事件冒泡。

二、事件处理程序

1. HTML 事件处理程序

<script> 
    function showMessage() { 
        console.log("Hello world!"); 
    } 
</script> 
<input type="button" value="Click Me" onclick="showMessage()"/>

作为事件处理程序执行的代码可以访问全局作用域中的一切。

2. DOM0 事件处理程序

第四代 Web 浏览器中开始支持的事件处理程序赋值方法,直到现在所有现代浏览器仍然都支持此方法,主要原因是简单。

let btn = document.getElementById("myBtn"); 
btn.onclick = function() { 
    console.log(this.id); // "myBtn"
};
  • this 引用元素本身
  • 通过 this 可以访问元素的任何属性和方法。
  • 以这种方式添加事件处理程序是注册在事件流的冒泡阶段
  • 若想移除 DOM0 方式添加的事件处理程序,可将事件处理程序属性的值设置为 null
btn.onclick = null; // 移除事件处理程序

3. DOM2 事件处理程序

DOM2 Events 为事件处理程序的赋值和移除定义了两个方法

  • addEventListener()
  • removeEventListener()

这两方法接收 3 个参数:事件名、事件处理函数和一个布尔值,true 表示在捕获阶段调用事件处理程序,false(默认值)表示在冒泡阶段调用事件处理程序。

let btn = document.getElementById("myBtn"); 
btn.addEventListener("click", () => { 
    console.log(this.id); 
}, false);

DOM2 Events 的主要特点有

  • DOM2 Events 主要优势是:可以为同一个事件添加多个事件处理程序
  • 多个事件处理程序以添加顺序来触发
  • 通过 addEventListener()添加的事件处理程序只能使用 removeEventListener()并传入与添加时同样的参数来移除。这意味着使用 addEventListener()添加的匿名函数无法移除
  • 大多数情况下,事件处理程序会被添加到事件流的冒泡阶段,主要原因是跨浏览器兼容性好。
let btn = document.getElementById("myBtn"); 

btn.addEventListener("click", () => { 
     console.log(this.id); 
}, false); 

btn.removeEventListener("click", function() { // 没有效果!
     console.log(this.id); 
}, false);

4. IE 事件处理程序

IE 实现了与 DOM 类似的方法

  • attachEvent()
  • detachEvent()

这两个方法接收两个同样的参数:事件处理程序的名字和事件处理函数。

IE Events 的主要特点

  • attachEvent()的第一个参数是"onclick",DOM2 Events中是'click'

  • 事件处理程序的作用域:

    (1) 使用 DOM0方式时,事件处理程序中的 this 值等于目标元素

    (2) 使用 attachEvent()时,事件处理程序是在全局作用域中运行的,因此 this 等于 window

var btn = document.getElementById("myBtn"); 
btn.attachEvent("onclick", function() { 
    console.log(this === window); // true 
});
  • 同DOM2 Events 也可以给一个元素添加多个事件处理程序。但事件处理程序会以添加它们的顺序 反向触发
  • 作为事件处理程序添加的匿名函数也无法移除

5. 跨浏览器 事件处理程序

要确保事件处理代码具有最大兼容性,只需要让代码在冒泡阶段运行即可。

var EventUtil = { 
    addHandler: function(element, type, handler) { 
        if (element.addEventListener) { 
            element.addEventListener(type, handler, false); 
        } else if (element.attachEvent) { 
            element.attachEvent("on" + type, handler); 
        } else { 
            element["on" + type] = handler; 
        } 
    }, 
    removeHandler: function(element, type, handler) { 
        if (element.removeEventListener) { 
            element.removeEventListener(type, handler, false); 
        } else if (element.detachEvent) { 
            element.detachEvent("on" + type, handler); 
        } else { 
            element["on" + type] = null; 
        } 
    } 
};

优点:这两个方法已经实现了跨浏览器添加和移除事件处理程序;

缺点

  • 没有解决所有跨浏览器一致性问题,比如 IE的作用域问题、多个事件处理程序执行顺序问题等。
  • 注意:DOM0 只支持给一个事件添加一个处理程序。好在 DOM0 浏览器已经很少有人使用了,所以影响应该不大。

三、事件对象

DOM 中发生事件时,所有相关信息都会被收集并存储在一个名为 event 的对象中。比如导致事件的元素、发生的事件类型,以及可能与特定事件相关的任何其他数据。

1. DOM 事件对象

  • DOM0 或 DOM2 指定事件处理程序,都会传入这个 event 对象。
let btn = document.getElementById("myBtn"); 
btn.onclick = function(event) { 
    console.log(event.type); // "click" 
}; 
btn.addEventListener("click", (event) => { 
    console.log(event.type); // "click" 
}, false);
  • 在事件处理程序内部,this 对象始终等于 currentTarget 的值,而 target 是触发事件的实际目标。
  • preventDefault() 方法 用于阻止特定事件的默认动作。前提是 事件对象的 cancelable 属性为true。
  • stopPropagation() 方法 用于立即阻止事件流在 DOM 结构中传播,取消后续的事件捕获或冒泡。
  • eventPhase 表示调用事件处理程序的阶段:1 代表捕获阶段,2 代表到达目标,3 代表冒泡阶段

2. IE 事件对象

属性/方法类型读写说明
cancelBubble布尔值读/写默认为 false,设置为 true 可以取消冒泡(与 DOM 的 stopPropagation()方法相同)
returnValue布尔值读/写默认为 true,设置为 false 可以取消事件默认行为(与 DOM 的 preventDefault()方法相同)
srcElement元素只读事件目标(与 DOM 的 target 属性相同)
type字符串只读触发的事件类型
  • 若事件处理程序是使用 DOM0 方式指定的,则 event 对象只是 window 对象的一个属性
var btn = document.getElementById("myBtn"); 
btn.onclick = function() { 
    let event = window.event; 
    console.log(event.type); // "click" 
};
  • 若事件处理程序是使用 attachEvent()指定的,则 event对象 (window对象属性) 会作为唯一的参数传给处理函数
var btn = document.getElementById("myBtn"); 
btn.attachEvent("onclick", function(event) { 
    console.log(event.type); // "click" 
});
  • this 值并不总是等于事件目标,因此推荐使用事件对象的 srcElement 属性代替 this。

    (1) 使用 DOM0方式时,事件处理程序中的 this 值等于目标元素。

    (2) 使用 attachEvent()时,事件处理程序是在全局作用域中运行的,因此 this 等于 window。

var btn = document.getElementById("myBtn"); 
// DOM 0 
btn.onclick = function() { 
    console.log(window.event.srcElement === this); // true 
}; 
// IE 的 attachEvent
btn.attachEvent("onclick", function(event) { 
    console.log(event.srcElement === this); // false 
});

3. 跨浏览器 事件对象

var EventUtil = {
    getEvent: function(event) { 
        return event ? event : window.event; 
    }, 
    getTarget: function(event) { 
        return event.target || event.srcElement; 
    }, 
    preventDefault: function(event) { 
        if (event.preventDefault) { 
            event.preventDefault(); 
        } else { 
            event.returnValue = false; 
        } 
    },  
    stopPropagation: function(event) { 
        if (event.stopPropagation) { 
            event.stopPropagation(); 
        } else { 
            event.cancelBubble = true; 
        } 
    },
    addHandler: function(element, type, handler) { 
        if (element.addEventListener) { 
            element.addEventListener(type, handler, false); 
        } else if (element.attachEvent) { 
            element.attachEvent("on" + type, handler); 
        } else { 
            element["on" + type] = handler; 
        } 
    }, 
    removeHandler: function(element, type, handler) { 
        if (element.removeEventListener) { 
            element.removeEventListener(type, handler, false); 
        } else if (element.detachEvent) { 
            element.detachEvent("on" + type, handler); 
        } else { 
            element["on" + type] = null; 
        } 
    } 
};

使用:

btn.onclick = function(event) { 
    // 获取事件对象
    let event = EventUtil.getEvent(event); 
    
    // 获取事件目标
    let target = EventUtil.getTarget(event);
    
    // 取消默认..
    EventUtil.preventDefault(event);

    // 取消冒泡
    EventUtil.stopPropagation(event);
};

四、内存与性能

  1. JavaScript 中,页面中事件处理程序的数量与页面整体性能直接相关。原因:
  • 每个函数都是对象,都占用内存空间,对象越多,性能越差。
  • 在指定事件处理程序中访问所需 DOM 会先期造成整个页面交互的延迟。

  1. 因此围绕着事件这块,若想优化内存与性能问题,可以:
  • 限制一个页面中事件处理程序的数量,因为它们会占用过多内存,导致页面响应缓慢;
  • 利用事件冒泡,事件委托可以解决限制事件处理程序数量的问题;
  • 最好在页面卸载之前 删除 所有事件处理程序

1. 事件委托

“过多事件处理程序”的解决方案是使用事件委托

事件委托就是利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件。如:给所有元素共同的祖先节点添加一个事件处理程序。

事件委托具有如下优点

  • document 对象随时可用,任何时候都可以给它添加事件处理程序(不用等待 DOMContentLoaded或 load 事件)。这意味着只要页面渲染出可点击的元素,就可以无延迟地起作用。
  • 节省 花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省 DOM 引用,也可以节省时间。
  • 减少整个页面所需的内存,提升整体性能。

2. 删除事件处理程序

  • 待删除的元素上如果有事件处理程序,就不会被垃圾收集程序正常清理。因此最好在删除DOM前手工删除它的事件处理程序

  • 在页面卸载后事件处理程序没有被清理,则它们仍然会残留在内存中。之后,浏览器每次加载和卸载页面(比如通过前进、后退或刷新),内存中残留对象的数量都会增加。因此在页面卸载前 最好先删除所有事件处理程序