本内容是《Web前端开发之Javascript视频》的课件,请配合大师哥《Javascript》视频课程学习。
DOM规范并没有包括所有浏览器支持的所有事件,很多浏览器实现了一些自定义的事件,这些事件后来都被HTML5所支持;
上下文菜单(contextmenu)事件:
Win95在PC应用程序中引入的上下文菜单的概念,即鼠标右击调出的菜单或者按下键盘上的菜单键时调出的菜单,后来,这个概念被引入Web领域;为了实现上下文菜单,需要确定何时应该显示上下文菜单,以及如何屏蔽与该操作关联的默认上下文菜单;为了解决这个问题,就出现了contextmenu这个事件,以便开发人员取消默认的上下文菜单而提供自定义的菜单;
document.addEventListener("contextmenu", function(event){
console.log(event); // MouseEvent
event.preventDefault();
},false);
contextmenu事件属于鼠标事件类型,所以其事件对象中包含与鼠标位置有关的所有属性;为了表明它是鼠标事件类型且是右击,所以其button值为2、which值为3;其target为发生用户操作的元素
该事件是冒泡的,因此可以为document指定一个事件处理程序,用以处理页面中所有此类事件;
通常使用contextmenu事件来显示自定义的上下文菜单,而使用onclick事件处理程序来隐藏该菜单;
<style>
#contextmenu{
width: 180px; height: 250px; background-color: lightblue; z-index: 999;
position: absolute; border: 1px solid gray; box-shadow: 2px 2px 3px #666;
}
.show{visibility:visible;}
.hidden{visibility: hidden;}
</style>
<script>
window.onload = function(){
var menu = document.createElement("div");
menu.id="contextmenu";
menu.className = "hidden";
document.body.appendChild(menu);
bindEvent(document, "contextmenu", openContextMenu);
bindEvent(document, "click", closeContextMenu);
function openContextMenu(event){
event.preventDefault();
event = event || window.event;
var button = event.button;
if(button == 2){
var pos = getContextMenuPosition(event);
menu.style.left = pos.left + "px";
menu.style.top = pos.top + "px";
menu.className = "show";
}
}
function closeContextMenu(event){
menu.className = "hidden";
}
// 防止菜单超出了边界
function getContextMenuPosition(event){
var x = event.clientX, y = event.clientY;
var vx = document.documentElement.clientWidth,
vy = document.documentElement.clientHeight;
var mw = menu.offsetWidth, mh = menu.offsetHeight;
return {
left: (x + mw) > vx ? (x - mw) : x,
top: (y + mh) > vy ? (y - mh) : y
}
}
function bindEvent(element, eventType, callback){
var ieType = "on" + eventType;
if(ieType in element)
element[ieType] = callback;
else if("attachEvent" in element)
element.attachEvent(ieType, callback);
else
element.addEventListener(eventType, callback, false);
}
}
</script>
beforeunload卸载前事件:
当浏览器窗口关闭或刷新时,会触发该事件,该事件应该注册在window对象上,当触发这个事件时,当前页面不会直接关闭,可以通过它来取消卸载并继续使用原有页面,目的是为了让开发人员有可能在页面卸载前阻止这一操作,但不能彻底取消这个事件,意图是将控制权交给用户,其会显示消息告知用户当前页面将要被卸载,询问用户是否真的要关闭页面,由用户来决定;
该事件不冒泡;
window.addEventListener("beforeunload", function(event){
debugger;
console.log(event); // BeforeUnloadEvent
});
在此事件中不能调用window.alert(),window.confirm()以及window.prompt()方法,因为对于beforeunload和unload事件,要求事件处理函数内部不能阻塞当前线程,而这些方法都会阻塞当前线程,因此H5规范中明确规定在beforeunload和unload中直接无视这几个方法的调用;
为了显示对话框,询问用户是否真的要离开该页面,根据规范,应该在事件处理程序中调用preventDefault()方法,但并不是所有浏览器都遵守这个规范,如:
window.addEventListener("beforeunload", function(event){
event.preventDefault();
console.log(event); // BeforeUnloadEvent
});
IE会显示一个默认的对话框(确实要离开此页吗?离开此面/留在此页),但其它浏览没有反应;
如果要实现自定义的提示,可以让事件处理程序返回一个字符串,或者将event.returnValue的值设置为要显示的字符,如:
return "确实要走吗?";
// 或
event.returnValue = "不要走啊";
IE会显示对话框,并且包括返回的字符串,但其它浏览器不支持,其他浏览器必须将它作为函数的值返回,如:
window.addEventListener("beforeunload", function(event){
event.preventDefault();
return event.returnValue = "不要走啊";
});
示例:自动保存数据:
<form>
<input id="username" type="text" />
<input id="userage" type="text" />
</form>
<script>
window.onload = function(event){
var obj = localStorage.getItem("userObj");
if(obj){
obj = JSON.parse(obj);
var username = document.getElementById("username");
var userage = document.getElementById("userage");
username.value = obj.username;
userage.value = obj.userage;
var h1 = document.createElement("h1");
h1.innerHTML = "自动保存的数据:";
username.parentNode.insertBefore(h1, username);
}
}
window.addEventListener("beforeunload", function(event){
var username = document.getElementById("username");
var userage = document.getElementById("userage");
var obj = {};
if(username.value){
obj.username = username.value;
}
if(userage.value){
obj.userage = userage.value;
}
if(obj)
localStorage.setItem("userObj", JSON.stringify(obj));
});
</script>
beforeunload先于unload事件触发;
经常会有一些在用户离开页面前执行一些业务的应用场景,这都要用到onbeforeunload事件;比如记录用户停留时长的业务,在GA等页面访问统计的应用中都包含这个:
(function(){
var startTime = Math.ceil(new Date().getTime() / 1000),
getDuration = function(){
var time="", hours = 0, minutes=0, seconds = 0,
endTime = Math.ceil(new Date().getTime() / 1000),
duration = endTime - startTime;
hours = Math.floor(duration / 3600);
minutes = Math.floor(duration % 3600 / 60);
seconds = Math.floor(duration % 3600 % 60);
time = (hours < 10 ? "0" + hours : hours) + ":" + (minutes < 10 ? "0" + minutes : minutes) + ":" + (seconds < 10 ? "0" + seconds : seconds);
return time;
};
window.onbeforeunload = function(e){
var duration = getDuration();
// submit duration
}
})();
DOMContentLoaded事件:
window的load事件会在页面中的一切都加载完毕时触发,但这个过程可能会因为要加载的外部资源过多而等待的时间过长;DOMContentLoaded事件则在形成完整的DOM树之后就会触发,不需要等待图像、JS文件、CSS文件或其他资源是否已经下载完毕;
利用此事件,可以为document或window添加事件处理程序;
<img src="images/1.jpg" width="100" />
<script>
document.addEventListener("DOMContentLoaded", function(event){
console.log("DOM准备完毕");
debugger;
console.log(event); // Event
},false);
</script>
该事件对象是Event类型,其不会提供任何额外的信息,也可以注册在window对象上,其target是document;
该事件始终会在load事件前触发,因此,该事件的目的,就是支持在页面下载的早期添加事件处理程序,使用户能够尽早地与页面进行交互;;
IE8及以下浏览器不支持该事件,可以在页面加载期间设置一个时间为0毫秒的超时调用:在页面下载和重构期时,只有一个js处理过程,因此超时调用会在该过程结束时立即触发;至于这个时间与DOMContentLoaded的时间能否同步,主要取决于浏览器和页面中的其他代码;为了确保有效,必须将其作为页面中的第一个超时调用;即使如此,也无法保证在所有环境中一定会早于load事件被触发,如:
setTimeout(function(){
console.log("DOMContentLoaded"); // 后执行
}, 0);
console.log("begin"); // 先执行
还有两种方案:
一种是创建空script标签,属性拥有defer,然后待onreadystatechange为complete时激发DOMContentLoaded;
document.attachEvent("onreadystatechange", function(event){
alert(document.readyState);
});
一种是通过调用doScroll('left')的原理去判断DOMContentLoaded,其基本思路是通过反复尝试执行来检测document.documentElement.doScroll("left"),因为在DOM树未创建完之前调用doScroll会抛出错误,如果没有抛出错误,就意味着DOM准备就绪了;
function doScroll(){
try{
document.documentElement.doScroll("left");
}catch(error){
setTimeout(doScroll,50);
return;
}
// 没有错误,表示DOM树创建完毕,可以执行
ready();
}
function ready(){
console.log("DOM Ready");
}
doScroll();
另外一种方案:
function ready(callback){
if(document.addEventListener){
document.addEventListener("DOMContentLoaded", function(event){
document.removeEventListener("DOMContentLoaded", arguments.callee, false);
callback();
},false)
}else if(document.attachEvent){
// 如果是IE
// 确保当页面是在iframe中加载时,事件依旧会被安全触发
document.attachEvent("onreadystatechange", function(evnet){
if(document.readyState == "complete"){
document.detachEvent("onreadystatechange", arguments.callee);
callback();
}
});
// 如果是IE且页面上不在iframe中时,轮询调用doScroll方法检测DOM是否加载完毕
if(document.documentElement.doScroll && typeof window.frameElement === "undefined"){
try{
document.documentElement.doScroll("left");
}catch(error){
return setTimeout(arguments.callee, 50);
}
callback();
}
}
}
或者:
function bindReady(handler){
var called = false;
// 确保handler只执行一次
function ready(){
if (called) return;
called = true;
handler();
}
if(document.addEventListener)
document.addEventListener("DOMContentLoaded", ready, false);
else if(document.attachEvent){
try{
var isFrame = window.frameElement != null; // 是否在框架中
}catch(e) {}
// 如果是IE并且不在iframe中
if (document.documentElement.doScroll && !isFrame) {
function tryScroll(){
if(called) return;
try{
document.documentElement.doScroll("left");
ready();
}catch(e){
setTimeout(tryScroll, 10);
}
}
tryScroll();
}
// 如果是IE并且在iframe中
document.attachEvent("onreadystatechange", function(){
if(document.readyState === "complete") {
ready();
this.onreadystatechange = null;
}
});
}
// 其他旧版本的浏览器,只能注册在load事件中
if(window.addEventListener){
window.addEventListener('load', ready, false);
}else if(window.attachEvent){
window.attachEvent('onload', ready);
}else{
// very old browser, copy old onload
var fn = window.onload
// replace by new onload and call the old one
window.onload = function() {
fn && fn();
ready();
}
}
}
bindReady(function(){
alert("DOM Ready");
});
readystatechange就绪状态变化事件:
IE为DOM文档中的某些部分提供了readystatechange事件,该事件的目的是提供与文档或元素的加载状态有关的信息;当某个对象的readyState属性发生改变时,就会触发该事件;
document.addEventListener("readystatechange", function(event){
console.log(document.readyState); // interactive complete
console.log(event); // Event
});
for(var i=0; i<1000000000; i++){}
支持该事件的每个对象都有一个readyState属性,可能包含以下5个值之一;
- uninitialized(未初始化):对象存在但尚未初始化;
- loading(正在加载):对象正在加载数据;
- loaded(加载完成):对象加载数据完成
- interactive(交互):可以操作对象了,但还没有完全加载;
- complete(完成):对象已经加载完毕;
并非所有对象都会经历该事件的几个阶段,即如果某个阶段不适用某个对象,则该对象完全可能跳过该阶段;
对于document而言,会有loading、interactive和complete三个阶段,如:
document.addEventListener("readystatechange", function(event){
switch (document.readyState) {
case "loading":
console.log("loading,表示文档正在加载中");
break;
case "interactive":
console.log("文档结束了loading状态,DOM元素可以被访问,但是像图像、样式表和框架等资源依然还在加载");
var span = document.createElement("span");
span.textContent = "已加载了DOM";
document.body.appendChild(span);
break;
case "complete":
console.log("页面所有内容都已被完全加载");
var rule = document.styleSheets[0].cssRules[0].cssText;
console.log(rule);
break;
}
});
当readyState状态为”interactive” 时触发的readystatechange事件,其与DOMContentLoaded事件发生的时间大致相同;
模拟DOMContentLoaded事件:
// DOM interactive -> DOM Loaded
document.addEventListener("readystatechange", function(event){
if(document.readyState == "interactive")
console.log("DOM interactive");
},false);
document.addEventListener("DOMContentLoaded", function(event){
console.log("DOM Loaded");
},false);
当readyState值为complete时,与load事件发生的时间也大致相同,但总是在load事件前发生;
模拟load事件,如:
// complete -> Loaded
document.addEventListener("readystatechange", function(event){
if(document.readyState == "complete")
console.log("complete");
},false);
window.addEventListener("load", function(event){
console.log("Loaded");
},false);
在interactive和complete之间,可以对DOM操作,或进行一些准备操作,以便加快页面的加载速度,如:
document.addEventListener('readystatechange', event => {
if (event.target.readyState === 'interactive') {
initLoader();
}
else if (event.target.readyState === 'complete') {
initApp();
}
});
function initLoader(){
var img = document.createElement("img");
img.src = "images/3.jpg";
document.body.appendChild(img);
alert("ok"); // 此时页面是空白
}
function initApp(){
var img = document.querySelector("img");
alert(img.width + ":" + img.height);
}
但是,interactive交互阶段可能会早于也可能会晚于complete完成阶段出现,无法确保顺序,因此,为了尽可能抢到先机,有必在同时检测交互和完成阶段;
document.addEventListener("readystatechange", function(event){
if(document.readyState == "interactive" || document.readyState == "complete"){
document.removeEventListener("readystatechange", arguments.callee);
console.log("Content Loaded");
}
},false);
虽然使用readystatechange可以十分近似地模拟DOMContentLoaded事件,但它们本质上还是不同的;
// 顺序为interactive->DOMContentLoaded->complete->load
var handler = function(event){
console.log(event.type);
console.log(event.timeStamp);
console.log(document.readyState);
console.log("\n");
}
document.addEventListener("readystatechange", handler);
document.addEventListener("DOMContentLoaded", handler,false);
window.addEventListener("load", handler,false);
load事件与readystatechange事件,在极个别的情况下,无法预测两个事件触发的先后顺序,如果页面中存在大量资源的时候,readystatechange可能会在onload事件之后才触发;
示例:通过load、readystatechange和DOMContentLoaded事件来判断DOM是否准备好,如果准备好,可以执行回调函数;
function domReady(fn) {
var ready = false,
top = false,
doc = window.document,
root = doc.documentElement,
modern = doc.addEventListener,
add = modern ? 'addEventListener' : 'attachEvent',
del = modern ? 'removeEventListener' : 'detachEvent',
pre = modern ? '' : 'on',
init = function(e) {
if(e.type === 'readystatechange' && doc.readyState !== 'complete')
return;
(e.type === 'load' ? window : doc)[del](pre + e.type, init, false);
if (!ready && (ready = true))
fn.call(window, e.type || e);
},
poll = function() {
try {
root.doScroll('left');
} catch(e) {
// setTimeout(pull, 50); // IE7可能不支持,用下面的代替
setTimeout(arguments.callee, 50);
return;
}
init('poll');
};
if(doc.readyState === 'complete')
fn.call(window, 'lazy');
else {
if(!modern && root.doScroll) {
try {
top = !window.frameElement;
} catch(e) {}
if (top)
poll();
}
doc[add](pre + 'DOMContentLoaded', init, false);
doc[add](pre + 'readystatechange', init, false);
window[add](pre + 'load', init, false);
}
}
domReady(function(){
console.log("DOM ready");
});
在IE10以下,<script>和<link>元素也会触发readystatechange事件,可以用来确定外部的JS和CSS文件是否已加载完成;但其他浏览器都不支持;
另外,readystatechange事件在其他API也存在,例如XMLHttpRequest中;
示例:包装whenReady()函数,以监听DOMContentLoaded和readystatechange事件,如:
var whenReady = (function(){ // 这个函数返回whenReady()函数
var funcs = []; // 当获得事件时,要运行的函数
var ready = false; // 当触发事件处理程序时,切换到true
// 当文档准备就绪时,调用事件处理程序
function handler(e){
// 如果已经运行过一次,只需要返回
console.log(document.readyState);
if(ready) return;
// 如果发生readystatechange事件,但其状态不是complete,那么文档尚未准备好
if(e.type === "readystatechange" && document.readyState !== "complete")
return;
// 运行所有注册函数,注意每次都要计算funcs.length
// 以防这些函数的调用可能会导致注册更多的函数
for(var i=0; i<funcs.length; i++)
funcs[i].call(document);
// 设置ready为true,并移除所有函数
ready = true;
funcs = null;
}
// 为接收到的任何事件注册处理程序
if(document.addEventListener){
document.addEventListener("DOMContentLoaded", handler, false);
document.addEventListener("readystatechange", handler, false);
window.addEventListener("load", handler, false);
}else if(document.attachEvent){
document.attachEvent("onreadystatechange", handler);
window.attachEvent("onload", handler);
}
// 返回whenReady()函数
return function whenReady(f){
if(ready) f.call(document); // 若准备完毕,就运行
else funcs.push(f); // 否则,加入队列等候
};
}());
// 应用
function show(){console.log("show")}
whenReady(show);
function insertEle(){
var div = document.createElement("div");
div.innerHTML = "Web前端开发";
document.body.appendChild(div);
console.log("已添加");
}
whenReady(insertEle);
hashchange事件:
是HTML5新增的事件,以便在URL的参数列表(即URL中“#”号后面的所有字符串)发生变化时通知开发人员;之所以增加这个事件,是因为在Ajax应用中,开发人员经常要利用URL参数列表来保存状态或导航信息;
必须要把hashchange事件处理程序添加给window对象,然后URL参数列表只要变化就会调用它;
window.addEventListener('hashchange', function(event) {
console.log('hash改变了');
console.log(location.hash); // #...
console.log(event); // HashChangeEvent
}, false);
HashChangeEvent类:
表示一个变化事件,当 URL 中的片段标识符发生改变时,会触发此事件;片段标识符指 URL 中 # 号和它以后的部分;其继承自Event类;
HashChangeEvent对象额外包含两个属性:oldURL和newURL,分别保存着参数列表变化前后的完整URL(也就是oldURL保存的是跳转之前的URL,newURL保存的是即将跳转的新URL),如:
EventUtil.addHandler(window, "hashchange", function(event){
console.log("old URL:" + event.oldURL);
console.log("new URL:" + event.newURL);
});
除了IE7及以下不支持hashchange事件,其他所有浏览器都支持;
IE7以上版本虽然支持hashchange事件,但把HashChangeEvent类当作普通的Event,所以不支持HashChangeEvent对象的这两个属性,但可以使用location对象来确定当前的参数列表,如:
console.log("new URL:" + event.newURL);
console.log("current hash:" + location.hash);
console.log("location URL:" + window.location.href);
检测浏览器是否支持hashchange事件,如:
var isSupported = ("onhashchange" in window) // 有Bug
console.log(isSupported);
如果IE8是在IE7文档模式下运行或IE7以下浏览器,即使功能无效,依然会返回true,可以采取更稳妥的检测方式:
var isSupported = ("onhashchange" in window) && (document.documentMode === undefined || document.documentMode > 7);
示例:
window.onhashchange = function(event){
console.log("oldURL:" + event.oldURL);
console.log("newURL:" + event.newURL);
};
(function(window){
// 如果浏览器已经实现了此事件,则退出函数
var isSupported = ("onhashchange" in window) &&
(document.documentMode === undefined ||
document.documentMode > 7);
if(isSupported) return;
var location = window.location,
oldURL = location.href,
oldHash = location.hash;
// 每隔100ms,检查一次hash
setInterval(function(){
var newURL = location.href,
newHash = location.hash;
// 如果hash有变化,且处理程序存在
if(newHash != oldHash && typeof window.onhashchange === "function"){
// 执行处理程序,并传一个伪装的hashchangeevent对象
window.onhashchange({
type: "hashchange",
oldURL: oldURL,
newURL: newURL
});
// 及时更新oldURL和oldHash,否则会一直触发
oldURL = newURL;
oldHash = newHash;
}
},100);
})(window);
HTML5还拥有其他大量事件,例如有关audio和video多媒体事件,拖放事件,历史管理事件等;
另外还有设备事件(就是移动端设备),包括触摸和手势事件;
内存和性能:
在JS中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能;如:每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差;必须事先指定所有事件处理程序而导致的DOM访问次数,会延迟整个页面的交互就绪时间;
事件委托:
事件委托利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件;如:
<!-- 传统做法 -->
<ul id="mylist">
<li id="item1">HTML</li>
<li id="item2">CSS</li>
<li id="item3">Javascript</li>
</ul>
<script>
var item1 = document.getElementById("item1");
var item2 = document.getElementById("item2");
var item3 = document.getElementById("item3");
EventUtil.addHandler(item1, "click", function(event){
location.href = "https://www.zeronetwork.cn/";
});
EventUtil.addHandler(item2, "click", function(evnet){
event = EventUtil.getEvent(event);
document.title = EventUtil.getTarget(event).innerText;
});
EventUtil.addHandler(item3, "click", function(event){
console.log("Web前端开发");
})
</script>
使用事件委托,只需要在DOM树中尽量最高的层次上添加一个事件处理程序,如:
var list = document.getElementById("mylist");
EventUtil.addHandler(list, "click", function(event){
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
switch(target.id){
case 'item1':
location.href = "https://www.zeronetwork.cn/";
break;
case 'item2':
document.title = target.innerText;
break;
case 'item3':
console.log("Web前端开发");
break;
}
});
如果可行的话,可以考虑为document对象添加一个事件处理程序,用以处理页面上发生的某种特定类型的事件,这样杉的优点是:
document对象很快就可以访问:且在页面生命周期的任何点上都可以为它添加事件处理程序(无需等DOMContentLoaded或load事件);
在页面中设置事件处理程序所需的时间更少,只添加一个事件处理程序所需的DOM引用更少,所花时间也更少;
整个页面占用的内容空间更少,能够提升整体性能;
最适合采用事件委托技术的事件包括:click、mousedown、mouseup、keydown、keyup和keypress;虽然mouseover和mouseout事件也冒泡,但要适当处理它们并不容易,而且经常需要计算元素的位置;
移除事件处理程序:
每当事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的JS代码之间就会建立一个连接;这种连接越多,页面执行起来就越慢;可以采用事件委托技术,限制连接数量;另外,在不需要的时间移除事件处理程序;
内存在留有那些过时不用的“空事件处理程序”(dangling event handler),也是造成Web应用程序内存与性能问题的主要原因;
在两种情况下,可能会造成上述问题;第一种情况就是从文档中移除带有事件处理程序的元素时,这可能是通过纯粹的DOM操作,例如,使用removeChid()和replaceChild()方法,或使用innerHTML替换页面中某一部分的时候,其原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收;
<div id="mydiv">
<button id="btn">按钮</button>
</div>
<script>
var btn = document.getElementById("btn");
btn.onclick = function(){
//...
btn.onclick = null; // 移除事件处理程序
document.getElementById("mydiv").innerHTML = "Process...";
}
</script>
在设置div的innerHTML属性前,先移除按钮的事件处理程序;
还有一种方案,也就是采用事件委托的方式,例如,如果事先知道将来有可能使用innerHTML等方式替换页面中的一部分内容,那么就不要直接把事件处理程序注册到这部分中的元素上,而是注册到较高层次的、且不会被替换掉的元素上;
注意,在事件处理程序中删除目标元素也能阻止事件冒泡,目标元素在文档中是事件冒泡的前提;
在卸载页面的时候,如果没有清理干净事件处理程序,那它们就会滞留在内存中;每次加载完页面再卸载页面时,内存中滞留的对象数目就会增加,特别是IE;
因此,在卸载页面之前,先通过onunload事件处理程序移除所有事件处理程序;onunload就类似于“撤销”的操作,只要通过onload事件处理程序添加的东西,最后都要通过onunload事件处理程序将它们移除;