DOM规范并没有包括所有浏览器支持的所有事宜,很多浏览器实现了一些自定义的事宜,这些事宜后来都被HTML5所支持;

高下文菜单(contextmenu)事宜:

Win95在PC运用程序中引入的高下文菜单的观点,即鼠标右击调出的菜单或者按下键盘上的菜单键时调出的菜单,后来,这个观点被引入Web领域;为了实现高下文菜单,须要确定何时该当显示高下文菜单,以及如何屏蔽与该操作关联的默认高下文菜单;为理解决这个问题,就涌现了contextmenu这个事宜,以便开拓职员取消默认的高下文菜单而供应自定义的菜单;

html5鼠标点击事件第60节 HTML5事宜和事宜委托JavaScript零点法式员王唯 React
(图片来自网络侵删)

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 Loadeddocument.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 -> Loadeddocument.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->loadvar 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) // 有Bugconsole.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事宜处理程序将它们移除;