1. Event Handlers
<p>Click this document to activate the handler.</p>
<script>
addEventListener("click", function() {
console.log("You clicked!");
});
</script>
相当于调用 window.addEventListener( ),因为,window对象是所有全局变量/函数的容器。
在浏览器窗口中点击鼠标,控制台输出:You clicked!2. Events and DOM nodes
每一个DOM node都有自己的addEventListener
<button>Click me</button>
<p>No handler here.</p>
<script>
var button = document.querySelector("button");
button.addEventListener("click", function() {
console.log("Button clicked.");
});
</script>
onclick 属性,和addEventListener 的作用相同
<button οnclick="onBtnClicked()">Click me</button>
<script>
function onBtnClicked() {
console.log("Button clicked.");
}
</script>
addEventListener 可以给一个事件添加多个handler,而,onclick 属性不行
删除事件句柄: removeEventListener()
<button>Act-once button</button>
<script>
var button = document.querySelector("button");
function once() {
console.log("Done.");
button.removeEventListener("click", once);
}
button.addEventListener("click", once);
</script>
3. Event Objects
<button οnmοusedοwn="onMouseDown(event)">Click me any way you want</button>
<script>
function onMouseDown(event) {
console.log(event.type);
if (event.which == 1)
console.log("Left button");
else if (event.which == 2)
console.log("Middle button");
else if (event.which == 3)
console.log("Right button");
}
</script>
通过event.which识别鼠标左、右键。
event.type 存储了event的名字,例如:mousedown, click。
4. Propagation
propagation
如果一个子节点收到click事件,它的父节点也会收到,父节点的父节点也会收到,直到window,都会收到click事件。
如果一个节点和它的父节点都有ckick事件的handler,那么,子节点的handler先执行,然后执行父节点的。 如果在子节点的handler中调用了 stopPropagtion( ) ,则父节点的handler不再执行。
stopPropagation()
<p>A paragraph with a <button>button</button>.</p>
<script>
var para = document.querySelector("p");
var button = document.querySelector("button");
para.addEventListener("mousedown", function() {
console.log("Handler for paragraph.");
});
button.addEventListener("mousedown", function(event) {
console.log("Handler for button.");
if (event.which == 3)
event.stopPropagation();
});
</script>
<button> 是 <p> 的子节点,二者都添加了 mousedown handler 。
用鼠标左键点击button,二者的handler都会执行。
用鼠标右键点击button,只有button的handler执行。因为,调用了event.stopPropagation( )
event.target
event 的接收目标,拥有该handler的对象。
<button>A</button>
<button>B</button>
<button>C</button>
<script>
document.body.addEventListener("click", function(event) {
if (event.target.nodeName == "BUTTON")
console.log("Clicked", event.target.textContent);
});
</script>
5. Default Actions
很多event有默认的action,例如:
1. <a> ,点击一个link,就会跳转到它的url。
2. 在浏览器中点击右键,会弹出上下文菜单。3. 在浏览器中按键盘上的 “下” 键,会使网页上下滚动。
对于大部分类型的event,“自定义的handler” 会优先于 “默认action” 执行。
preventDefault() 禁止默认action执行。例如:
<a href="https://developer.mozilla.org/">MDN</a>
<script>
var link = document.querySelector("a");
link.addEventListener("click", function(event) {
console.log("Nope.");
event.preventDefault();
});
</script>
尽量不要这么做,这会导致用户的困惑,影响网站的用户体验。
6. Key Events
6.1. keydown, keyup
<p>This page turns violet when you hold the V key.</p>
<script>
addEventListener("keydown", function(event) {
if (event.keyCode == 86)
document.body.style.background = "violet";
});
addEventListener("keyup", function(event) {
if (event.keyCode == 86)
document.body.style.background = "";
});
</script>
当按住一个键不松开,keydown 事件会被出发多次。
6.2. event.keyCode
对于字母和数字,keyCode就是它的Unicode编码,该编码和 string.charCodeAt( ) 结果相同。
console.log("Violet".charCodeAt(0));
// → 86
console.log("1".charCodeAt(0));
// → 49
对于其他特殊字符,没有一个标准的编码,需要自己去尝试。
6.3. Modifier Keys
Shift, Ctrl, Alt, Meta(Cmd on Mac)
单独按下这些键,也会产生正常的event,和其他键一样。
当使用组合键时,可以检查这些属性 shiftKey, ctrlKey, altKey, metaKey,例如:
<p>Press Ctrl-Space to continue.</p>
<script>
addEventListener("keydown", function(event) {
if (event.keyCode == 32 && event.ctrlKey)
console.log("Continuing!");
});
</script>
6.4. keypress
<p>Focus this page and type something.</p>
<script>
addEventListener("keypress", function(event) {
console.log(String.fromCharCode(event.charCode));
});
</script>
可见字符被按下才会触发这个event。
用String.fromCharCode(event.charCode) 可以得到被按下的字符。
和keydown一样,如果长时间按住一个键,会触发多次event。
6.5. focus普通的node不能接收到键盘event,除非给它加上 tabindex 属性。
link, button, form field 默认就可以获得键盘焦点。
如果网页中没有键盘焦点,document.body 会接收到键盘事件。
7. Mouse Clicks
7.1. mousedown, mouseup
7.2. click
既收到mousedown,又收到mouseup的node,会收到一个click event。
例如: 在一个<p> node 按下鼠标,鼠标保持按下,指针移动到另外一个<p>,抬起鼠标按键。那么,这两个<p> 都不会收到click event,而是,它们共同的父节点收到click event。
7.3. dblclick双击
7.4. 点击坐标
pageX, pageY 是以document左上角为原点的坐标。
7.5. 在网页上点蓝色圆点
<style>
body {
height: 200px;
background: beige;
}
.dot {
width: 8px; height: 8px;
background: blue;
border-radius: 4px;
position: absolute;
}
</style>
<script>
addEventListener('click', function (event) {
var dot = document.createElement("div");
dot.className = "dot";
dot.style.left = (event.pageX - 4) + "px";
dot.style.top = (event.pageY - 4) + "px";
document.body.appendChild(dot);
});
</script>
event.clientX 和 event.clientY 也是鼠标点击位置坐标,它们不是相对于document左上角的,而是浏览器的视窗左上角,当视窗向下滚动后,它们和pageX、pageY就不同了。
8.1. mousemove
鼠标移动时,触发此事件。
例如:拉动一个矩形,改变它的宽度
<p>Drag the bar to change its width:</p>
<div style="background: orange; width: 60px; height: 20px">
</div>
<script>
var lastX; // Tracks the last observed mouse X position
var rect = document.querySelector("div");
rect.addEventListener("mousedown", function(event) {
if (event.which == 1) {
lastX = event.pageX;
addEventListener("mousemove", moved);
event.preventDefault(); // Prevent selection
}
});
function buttonPressed(event) {
if (event.buttons == null)
return event.which != 0;
else
return event.buttons != 0;
}
function moved(event) {
if (!buttonPressed(event)) {
removeEventListener("mousemove", moved);
} else {
var dist = event.pageX - lastX;
var newWidth = Math.max(10, rect.offsetWidth + dist);
rect.style.width = newWidth + "px";
lastX = event.pageX;
}
}
</script>
注意:mousedown事件handler 加在了rect上,而mousemove 事件handler加在了window上。为什么这么做呢?在鼠标移动过程中,即使移动到了rect外部,也应该处理mousemove事件。
注意:buttonPressed 函数中,在判断鼠标左键是否按下时,不仅用到了event.which, 还用到了event.buttons。这是为了兼容所有浏览器。
8.2. mouseover, mouseout鼠标 "进入" 或 "离开" node。
propagation:父节点也会收到子节点的mouseover和mouseout 。
event.relatedTarget 是哪一个element:
对于mouseover事件: what element the pointer was over before.
对于mouseout事件: what element it is going to.
例子:
<p>Hover over this <strong>paragraph</strong>.</p>
<script>
var para = document.querySelector("p");
function isInside(node, target) {
for (; node != null; node = node.parentNode)
if (node == target) return true;
}
para.addEventListener("mouseover", function(event) {
if (!isInside(event.relatedTarget, para))
para.style.color = "red";
});
para.addEventListener("mouseout", function(event) {
if (!isInside(event.relatedTarget, para))
para.style.color = "";
});
</script>
9. Scroll Events
scroll,例子:自制进度条,显示当前窗口滚动的百分比。
<style>
.progress {
border: 1px solid blue;
width: 100px;
position: fixed;
top: 10px; right: 10px;
}
.progress > div {
height: 12px;
background: blue;
width: 0%;
}
body {
height: 2000px;
}
</style>
<div class="progress"><div></div></div>
<p>Scroll me...</p>
<script>
var bar = document.querySelector(".progress div");
addEventListener("scroll", function() {
var max = document.body.scrollHeight - innerHeight;
var percent = (pageYOffset / max) * 100;
bar.style.width = percent + "%";
});
</script>
10. Focus Events
10.1. events
blur: 失去焦点
focus: 获得焦点
这两个事件不会向 parentNode 传播。
10.2. 例子
显示当前获得焦点的 input 的 data-help 属性。
<p>Name: <input type="text" data-help="Your full name"></p>
<p>Age: <input type="text" data-help="Age in years"></p>
<p id="help"></p>
<script>
var help = document.querySelector("#help");
var fields = document.querySelectorAll("input");
fields.forEach(function (afield) {
afield.addEventListener("focus", function (event) {
var text = event.target.getAttribute("data-help");
help.textContent = text;
});
afield.addEventListener("blur", function (event) {
help.textContent = "";
});
});
</script>
11. Load Event
11.1. 页面加载完毕事件:
window.onload
document.body.onload
document.onload
三者的区别是什么?DOM加载完毕?还是整个页面所有资源加载完毕? 在不同版本的浏览器上定义不太一致,趋势是window.onload越来越流行。如果用jQuery的话,就不用太关心这个事情,直接用:
$(document).ready(function() { /* code here */ });
$(function() { /* code here */ });
11.2. external file loaded
外部文件加载完毕,也会收到 onload 事件。
11.3. beforeunload
关闭网页之时被调用
onbeforeunload = function (e) {
var dialogText = 'Dialog text here';
e.returnValue = dialogText;
return dialogText;
}
Starting with Firefox 4, Chrome 51, Opera 38 and Safari 9.1, a generic string not under the control of the webpage will be shown instead of the returned string.
12. Script Execution Timeline
12.1. start executing
js 代码何时开始执行?
在<body> 中遇到<script> 标签中的代码,立即开始执行。
requestAnimationFrame(myFunc) ,在浏览器刷新页面时,执行myFunc。
event触发时执行 event handler。
js 代码顺序执行,默认是不会并发的,即使是event触发的代码,也是顺次执行,执行完一个handler,再执行其他代码。
12.2. web worker
Worker 给js提供了一种异步并发机制。举个简单的例子:
// worker.html
<p>Count numbers: <output id="result"></output></p>
<button οnclick="startWorker()">Start Worker</button>
<button οnclick="stopWorker()">Stop Worker</button>
<br /><br />
<script>
var w;
function startWorker() {
if (typeof(Worker) != "undefined") {
if (typeof(w) == "undefined") {
w = new Worker("js/demo_worker.js");
}
w.onmessage = function (event) {
document.getElementById("result").innerHTML = event.data;
};
}
else {
document.getElementById("result").innerHTML = "Sorry, your browser does not support Web Worker...";
}
}
function stopWorker() {
w.terminate();
}
</script>
// js/demo_worker.js
var i=0;
function timedCount() {
i = i+1;
postMessage(i);
setTimeout("timedCount()", 500);
}
timedCount();
用一个js文件构造出Worker对象。这个Worker对象的代码会和其他 js 代码并发执行。
js/demo_worker.js 中,用 postMessage(xx) 与Worker对象通信: w.onmessage = function(event) { ... }
这个例子貌似没什么用处,只是演示了如何使用Worker。 当有比较繁重的任务需要并发时,可以写在 js/demo_worker.js 中。
13. Setting Timers
13.1. setTimeOut( ) / clearTimeOut( )
var bombTimer = setTimeout(function() {
console.log("BOOM!");
}, 500);
if (Math.random() < 0.5) { // 50% chance
console.log("Defused.");
clearTimeout(bombTimer);
}
13.2.
requestAnimationFrame() / cancelAnimationFrame()
13.3. setInterval() / clearInterval()
var ticks = 0;
var clock = setInterval(function() {
console.log("tick", ticks++);
if (ticks == 10) {
clearInterval(clock);
console.log("stop.");
}
}, 200);
14. Debouncing
目标:防止频繁多次响应一个event
用timeOut,如果间隔小于500ms,只处理最后一个event,前面的都cancel。
<textarea>Type something here...</textarea>
<script>
var textarea = document.querySelector("textarea");
var timeout;
textarea.addEventListener("keydown", function() {
clearTimeout(timeout);
timeout = setTimeout(function() {
console.log("You stopped typing.");
}, 500);
});
</script>
用timeOut,每隔固定时间响应一次。
<script>
function displayCoords(event) {
document.body.textContent =
"Mouse at " + event.pageX + ", " + event.pageY;
}
var scheduled = false, lastEvent;
addEventListener("mousemove", function(event) {
lastEvent = event;
if (!scheduled) {
scheduled = true;
setTimeout(function() {
scheduled = false;
displayCoords(lastEvent);
}, 250);
}
});
</script>
15. Exercise: Censored Keyboard
阻止用户输入Q,W,X:
<input type="text">
<script>
var field = document.querySelector("input");
field.addEventListener("keypress", function (event) {
if (/[qwx]/i.test(String.fromCharCode(event.charCode))) {
event.preventDefault();
}
});
</script>
16. Exercise: Mouse Trail
<style>
.trail { /* className for the trail elements */
position: absolute;
height: 6px; width: 6px;
border-radius: 3px;
background: teal;
}
body {
height: 300px;
}
</style>
<script>
var dots = [];
for (var i = 0; i < 12; i++) {
var node = document.createElement("div");
node.className = "trail";
document.body.appendChild(node);
dots.push(node);
}
var currentDot = 0;
addEventListener("mousemove", function(event) {
var dot = dots[currentDot];
dot.style.left = (event.pageX - 3) + "px";
dot.style.top = (event.pageY - 3) + "px";
currentDot = (currentDot + 1) % dots.length;
});
</script>
用十二个圆点跟随鼠标移动。
17. Exercise: Tabs
<div id="wrapper">
<div data-tabname="one">Tab one</div>
<div data-tabname="two">Tab two</div>
<div data-tabname="three">Tab three</div>
</div>
<script>
function asTabs(node) {
var tabs = [];
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes[i];
if (child.nodeType == document.ELEMENT_NODE)
tabs.push(child);
}
var tabList = document.createElement("div");
tabs.forEach(function(tab, i) {
var button = document.createElement("button");
button.textContent = tab.getAttribute("data-tabname");
button.addEventListener("click", function() { selectTab(i); });
tabList.appendChild(button);
});
node.insertBefore(tabList, node.firstChild);
function selectTab(n) {
tabs.forEach(function(tab, i) {
if (i == n)
tab.style.display = "";
else
tab.style.display = "none";
});
for (var i = 0; i < tabList.childNodes.length; i++) {
if (i == n)
tabList.childNodes[i].style.background = "violet";
else
tabList.childNodes[i].style.background = "";
}
}
selectTab(0);
}
asTabs(document.querySelector("#wrapper"));
</script>