关于事件的一些概念
- 事件源: 事件发生在哪个元素节点对象上, 谁就是这个事件的事件源
- 事件处理函数: 元素节点本身就是有事件的, 绑定事件: 是指添加事件处理函数, 当这个事情发生了之后, 应该做什么?
- 交互行为: 触发事件(比如点击) + 事件的反馈
绑定事件
绑定事件,绑定事件处理函数, 监听事件其实说的都是一回事, 就是当发生一个事件的时候, 程序如何处理? 比如: 当我点击按钮的时候, 应该去 "下载文件", 那么实现下载文件的这个功能的就是按钮点击事件的处理函数
- 句柄绑定事件(包括标签行内的绑定事件:
内联/行内事件监听器
)- 同一个的事件只能绑定一次, 绑定多次会覆盖
- js 代码绑定的事件比行内绑定的方式优先级高
- 可以直接把代码写到标签上
- 只能监听
冒泡阶段
,无法监听捕获阶段
- addEventListener 方法绑定事件(推荐 👍)
- IE9 以下不支持这种方式绑定
- 同一个类型的事件,可以绑定多个事件处理函数
- w3c DOM 规范的注册/绑定事件的方式
- 既可以监听
冒泡阶段
又可以监听捕获阶段
, 具体看第三个参数 mdn 文档
- attchEvent 方法绑定:
- IE8 及以下低版本的浏览器支持, chrom 是没有的
<!DOCTYPE html>
<html lang="en">
<head>
<title>js</title>
<style type="text/css">
#app {
background: black;
width: 100px;
height: 100px;
}
</style>
</head>
<body>
<div id="app" onclick="test" onmouseenter="console.log(1)"></div>
<!-- javascript -->
<script>
function test() {
console.log(1);
}
var app = document.getElementById("app");
app.onclick = function () {
console.info("click handler"); // 执行了这个, 证明js绑定的事件优先级高于内联事件监听器, 而且只能绑定一次
};
</script>
</body>
</html>
兼容低版本浏览器做法:
兼容低版本浏览器做法: 这里使用了惰性函数, 可以参考:
/**
* 兼容: 添加事件处理函数
* @param {HTMLElement} el 事件源
* @param {String} type 事件类型
* @param {Function} handler 事件处理函数
* @returns
*/
function addEvent(el, type, handler) {
if (el.addEventListener) {
addEvent = function (el, type, handler) {
el.addEventListener(type, handler, false);
};
} else if (el.attachEvent) {
addEvent = function (el, type, handler) {
el.attachEvent("on" + type, function () {
handler.call(el);
});
};
} else {
addEvent = function (el, type, handler) {
el["on" + type] = handler;
};
}
return addEvent(el, type, handler); // 只有这一次执行会判断
}
/**
* 兼容: 移除事件处理函数
* @param {HTMLElement} el 事件源
* @param {String} type 事件类型
* @returns
*/
function removeEvent(el, type, handler) {
if (el.removeEventListener) {
removeEvent = function (el, type, handler) {
el.removeEventListener(type, handler);
};
} else if (el.detachEvent) {
removeEvent = function (el, type, handler) {
el.detachEvent("on" + type, handler);
};
} else {
removeEvent = function (el, type, handler) {
el["on" + type] = null;
};
}
return removeEvent(el, type, handler); // 只有这一次执行会判断
}
面试题:
闭包问题: 复习闭包那一节的笔记 https://www.yuque.com/liaohui5/js-base/fepwdu Q: 以下代码, 控制台会输出什么? 为什么? A: 输出 5 个 A, 运维绑定事件处理函数是, 这个函数并没有执行, 所以最后执行的时候, 获取 i 的值是 for 循环执行玩之后的值
Q: 如果想让它输出对应序号应该怎么做?? A: 如果是 ES6, 直接把 var i = 0
修改为 let i = 0
就可以, 因为 let
关键字没有变量提升的问题 如果是 ES3, 在循环的时候用 闭包
去保存 i 变量的值, 然后再绑定事件就没有问题了
<!DOCTYPE html>
<html lang="en">
<head>
<title>js</title>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<!-- javascript -->
<script>
var lis = document.getElementsByTagName("li");
var item;
for (var i = 0, len = lis.length; i < len; i++) {
item = lis[i];
item.addEventListener(
"click",
function () {
console.info(i);
},
false
);
// 如果想拿到对应 i 的值:
// (function(i) {
// item = lis[i];
// item.addEventListener('click', function() {
// console.info(i);
// }, false);
// })(i);
}
</script>
</body>
</html>
移除事件处理函数
removeEventListener
方法detachEvent
,对应attachEvent
方法el.onclick = null
句柄绑定事件的方式重新赋值
<!DOCTYPE html>
<html lang="en">
<head>
<title>js</title>
<style>
#app {
width: 100px;
height: 100px;
background: #000;
}
</style>
</head>
<body>
<div id="app"></div>
<!-- javascript -->
<script>
var app = document.getElementById("app");
// 点击事件处理函数
var clickHandler = function () {
// 只会输出一次, 因为输出以后, 这个事件就被移除了
console.info("clicked");
// 点击事件处理函数
this.removeEventListener("click", clickHandler);
};
// 鼠标划入事件处理函数
var mouseEnterHandler = function () {
console.info("move enter");
// 移除鼠标划入事件处理函数
this.onmouseenter = null;
};
app.addEventListener("click", clickHandler, false);
app.onmouseenter = mouseEnterHandler;
</script>
</body>
</html>
事件捕获现象 & 事件冒泡现象
事件冒泡 event bubbling
当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序, 然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。
<!DOCTYPE html>
<html lang="en">
<head>
<title>js</title>
<style>
#wrapper {
width: 300px;
height: 300px;
background: red;
}
#outer {
width: 200px;
height: 200px;
background: green;
margin-left: 300px;
}
#inner {
width: 100px;
height: 100px;
background-color: blue;
margin-left: 200px;
}
</style>
</head>
<body>
<div id="wrapper">
<div id="outer">
<div id="inner"></div>
</div>
</div>
<!-- javascript -->
<script>
var wrapper = document.getElementById("wrapper");
var outer = document.getElementById("outer");
var inner = document.getElementById("inner");
var useCapture = false;
wrapper.addEventListener(
"click",
function () {
console.info("wrapper");
},
useCapture
);
outer.addEventListener(
"click",
function () {
console.info("outer");
},
useCapture
);
inner.addEventListener(
"click",
function () {
console.info("inner");
},
useCapture
);
</script>
</body>
</html>
事件捕获 event capturing
<!DOCTYPE html>
<html lang="en">
<head>
<title>js</title>
<style>
#wrapper {
width: 300px;
height: 300px;
background: red;
}
#outer {
width: 200px;
height: 200px;
background: green;
margin-left: 300px;
}
#inner {
width: 100px;
height: 100px;
background-color: blue;
margin-left: 200px;
}
</style>
</head>
<body>
<div id="wrapper">
<div id="outer">
<div id="inner"></div>
</div>
</div>
<!-- javascript -->
<script>
var wrapper = document.getElementById("wrapper");
var outer = document.getElementById("outer");
var inner = document.getElementById("inner");
var useCapture = true;
wrapper.addEventListener(
"click",
function () {
console.info("wrapper");
},
useCapture
);
outer.addEventListener(
"click",
function () {
console.info("outer");
},
useCapture
);
inner.addEventListener(
"click",
function () {
console.info("inner");
},
useCapture
);
</script>
</body>
</html>
addEventListener 的第三个参数的作用
MDN 文档: addEventListener(type, handler, useCapture || options)
- type: 字符串, 绑定的事件类型
- handler: 函数, 事件处理函数
- useCpature: 布尔值, 指定处理函数实在哪个阶段执行
true: 事件捕获
false: 事件冒泡
- options: 对象
- options.captcha: 默认 false, 是否监听捕获阶段
- options.once: 默认 false, 执行一次监听函数后就移除监听函数
- options.passive: 默认 false, 但是部分浏览器给
window, document, document.body
的某一些事件(比如:touchstart
)做了性能优化, 将默值改为了true
, 当值为 true 的时候, 就无法调用,preventDefault()
方法(会抛出异常
), 也就是说passive 为 true 就无法阻止默认行为
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>测试 Event 对象的 isTrusted 属性</title>
<style>
#app {
margin: 0;
padding: 0;
width: 100%;
height: 1500px;
}
</style>
</head>
<body>
<div id="app">可滚动内容</div>
<script>
var app = document.querySelector("#app");
// app 的 touchstart 事件监听函数可以正常执行
app.addEventListener("touchstart", function (e) {
e.preventDefault();
console.log("[app] e.defaultPrevented: ", e.defaultPrevented);
});
// document 的 touchstart 事件监听函会抛出异常:
document.addEventListener("touchstart", function (e) {
// [Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive.
// See https://www.chromestatus.com/feature/5093566007214080
e.preventDefault();
console.log("[app] e.defaultPrevented: ", e.defaultPrevented);
});
/***************************************
为什么 passive 参数设置为 true 能够挺高滚屏性能?
function handler(e) {
// 1. touchstart 的默认行为是滚动
// 2. 事件监听函数的执行顺序:
// 2.1 先执行 handler
// 2.2 然后执行默认行为
console.log("handler-exec");
}
document.addEventListener('touchstart', handler, { passive: true });
passive 参数设置为 true 能够挺高滚屏性能的原因:
1. 阻止默认行为的方法不会调用( preventDefault() )
2. 用两线程出处理问题, 一个处理事件监听函数, 一个处理默认行为
***************************************/
</script>
</body>
</html>
捕获和冒泡的执行顺序
- 其他元素节点: 先捕获, 后冒泡
- 事件源元素节点: 谁先绑定, 谁先执行
- 事件捕获和冒泡的执行顺序都是按照 DOM 结构来触发的, 跟谁 js 监听代码在前, 谁在后没有关系
<!DOCTYPE html>
<html lang="en">
<head>
<title>js</title>
<style>
#wrapper {
width: 300px;
height: 300px;
background: red;
}
#outer {
width: 200px;
height: 200px;
background: green;
margin-left: 300px;
}
#inner {
width: 100px;
height: 100px;
background-color: blue;
margin-left: 200px;
}
</style>
</head>
<body>
<div id="wrapper">
wrapper
<div id="outer">
outer
<div id="inner">inner</div>
</div>
</div>
<!-- javascript -->
<script>
var wrapper = document.getElementById("wrapper");
var outer = document.getElementById("outer");
var inner = document.getElementById("inner");
// ----------------------------- inner ------------------------------//
inner.addEventListener(
"click",
function () {
console.info("inner capture");
},
true
);
inner.addEventListener(
"click",
function () {
console.info("inner bubble");
},
false
);
// ----------------------------- wrapper ------------------------------//
wrapper.addEventListener(
"click",
function () {
console.info("wrapper capture");
},
true
);
wrapper.addEventListener(
"click",
function () {
console.info("wrapper bubble");
},
false
);
// ----------------------------- outer ------------------------------//
outer.addEventListener(
"click",
function () {
console.info("outer capture");
},
true
);
outer.addEventListener(
"click",
function () {
console.info("outer bubble");
},
false
);
</script>
</body>
</html>
取消冒泡/阻止浏览器默认事件
取消(停止)冒泡
有时候我们并不需要这样的冒泡, 所以就需要手动的去停止冒泡
/**
* 取消冒泡
* @param {Event} e
* @returns
*/
function cancelBubble(e) {
if (e.stopPropagation) {
cancelBubble = function (e) {
e = e || window.event;
return e.stopPropagation();
};
} else {
cancelBubble = function (e) {
e = e || window.event; // window.event 兼容ie8及其以下版本的浏览器
e.cancelBubble = true;
};
}
return cancelBubble(e); // 只会判断这一次
}
阻止浏览器默认事件
浏览器默认有很多事件: 比如点击 a 标签就直接跳转了, 比如点击鼠标右键会出现一个菜单, 比如点击表单中的按钮会直接提交表单
// 取消浏览器默认事件
function preventDefault(e) {
var e = e || widow.event;
if (e.preventDefault) {
// w3c standard
preventDefault = function (e) {
e.preventDefault(e);
};
} else {
// ie: 9/8/7/6
preventDefault = function (e) {
e = e || widow.event;
e.returnValue = false;
};
}
return preventDefault(e);
}
不能冒泡的事件类型
事件 | 说明 |
---|---|
abort | 音/视频终止加载 |
resize | 调整浏览器窗口大小 |
error | 静态加载失败(比如图片) |
load | 浏览器加载完网页(window.onload) |
unload | 浏览器关闭网页时 |
mouseenter | 鼠标移入 |
mouseleave | 鼠标移出 |
blur | 失去焦点 |
focus | 获取焦点 |
Event 对象的其他属性&方法
target / currentTarget
target: 始终指向初次触发事件的 DOM 对象, 事件源
currentTarget: 指向当前触发事件的 DOM 对象
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>测试 Event 对象的 target/currentTarget 属性</title>
</head>
<body>
<div id="wrapper">
<button id="btn">button</button>
</div>
<script>
function handler(e) {
// 点击按钮:
// target: 始终指向 #btn
// currentTarget: 第一次指向 #btn, 然后冒泡, 第二次指向 #wrapper
// target 可以做事件代理, 而 currentTarget 不行, 因为冒泡时, 他指向的 DOM 变了
console.info("target:", e.target);
console.info("currentTarget:", e.currentTarget);
}
document.querySelector("#wrapper").addEventListener("click", handler);
document.querySelector("#btn").addEventListener("click", handler);
</script>
</body>
</html>
cancelable 事件是否可以通过阻止默认行为取消
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>测试 Event 对象的 cancelable 属性</title>
<style>
.scroll-wrapper {
width: 100%;
height: 1500px;
}
</style>
</head>
<body>
<form action="" id="form">
<input type="text" />
<button type="submit">submit form</button>
</form>
<div class="scroll-wrapper">这是可以滚动内容</div>
<script>
function handler(event) {
if (typeof event.cancelable !== "boolean" || event.cancelable) {
// 如果当前被触发的事件可以取消就直接取消
event.preventDefault();
console.info(event.type + "事件被取消了", event);
} else {
// 如果不能取消
console.warn(event.type + "事件不能取消", event);
}
}
document.getElementById("form").addEventListener("submit", handler);
document.addEventListener("wheel", handler);
</script>
</body>
</html>
bubbles 当前事件是否会向 DOM 树上层元素冒泡
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>测试 Event 对象的 bubbles 属性</title>
</head>
<body>
<div id="div">
<input type="text" id="ipt" />
<button id="btn">button</button>
</div>
<script>
function handler(e) {
console.info(e);
if (typeof e.bubbles === "boolean" && e.bubbles) {
console.info(`${e.type} 事件会向上冒泡`, e);
} else {
console.warn(`${e.type} 事件不会向上冒泡`, e);
}
}
function $(selector) {
return document.querySelector(selector);
}
// focus 事件不会向上冒泡
$("#ipt").addEventListener("focus", handler);
// click 事件会向上冒泡
$("#btn").addEventListener("click", handler);
$("#div").addEventListener("click", function () {
console.info("div 的click事件被触发了");
});
</script>
</body>
</html>
defaultPrevented 判断当前事件是否调用了 preventDefault 方法
注意: 当 cancelbabel
为 false
或者 addEventListener
的 options.passive
为 true
时, preventDefault()
是无法取消的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>测试 Event 对象的 preventDefault 属性</title>
</head>
<body>
<div id="outter">
<button id="btn1">btn1</button>
<button id="btn2">btn2</button>
</div>
<script>
document.querySelector("#outter").addEventListener("click", function (e) {
if (e.target.id === "btn1") {
// 如果点击的是 #btn1 就执行阻止默认事件
e.preventDefault();
}
console.info("是否调用了 preventDefault: ", e.defaultPrevented);
});
</script>
</body>
</html>
eventPhase 当前事件流在哪个阶段
isTrusted: 判断事件是用户交互产生的, 还是 js 脚本手动创建的
true: 是用户操作产生的事件 false: js 脚本创建的事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>测试 Event 对象的 isTrusted 属性</title>
</head>
<body>
<button id="btn">click me</button>
<script>
var btn = document.querySelector("#btn");
btn.addEventListener("click", function (e) {
console.info("isTrusted: ", e.isTrusted, e);
});
// 脚本创建的事件, 手动调用触发, 必须先添加监听函数, 才能触发监听函数
const event = document.createEvent("MouseEvent");
event.initEvent("click");
btn.dispatchEvent(event);
</script>
</body>
</html>
stopPropagation() 阻止默认事件
只有事件的 cancelable
为 true
并且 addEventListener
的 options.passive
为 false 的时候才可以阻止
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>测试 Event 对象的 stopProgation 方法</title>
</head>
<body>
<form action="">
<input type="text" id="ipt" />
<button type="submit" id="submit-btn">submit form</button>
</form>
<script>
function handler(e) {
e.preventDefault();
console.info("事件是否被阻止: ", e.defaultPrevented, e);
}
// input: focus 无法被阻止, 因为 cancelable 为 false
document.querySelector("#ipt").addEventListener("focus", handler);
// form: submit 无法被阻止, 因为 addEventListener 的 options.passive 为 true
document.querySelector("#submit-btn").addEventListener("click", handler, {
passive: true, // 如果是 false 就可以阻止表单提交, 因为 click 事件的 cancalable 为 true
});
</script>
</body>
</html>
stopImmediatePropagation(): 阻止其他后续同类型的监听函数执行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>测试 Event 对象的 type 属性</title>
<style>
.red {
background: red;
}
.blue {
background: blue;
}
.item {
width: 100px;
height: 100px;
}
</style>
</head>
<body>
<div class="item red"></div>
<div class="item blue"></div>
<script>
var red = document.querySelector(".red");
var blue = document.querySelector(".blue");
red.addEventListener("click", function (e) {
console.info("red-click-1");
// e.stopImmediatePropagation();
// 如果在此处执行, 后面添加的 click 监听函数都不会执行
});
red.addEventListener("click", function (e) {
console.info("red-click-2");
e.stopImmediatePropagation();
// 如果在此处执行, 后面添加的 click 监听函数都不会执行,
// 但是前面添加的不受影响, 正常执行
});
red.addEventListener("click", function () {
console.info("red-click-3");
});
// 不阻止, 会按照顺序添加监听函数并执行
blue.addEventListener("click", function () {
console.info("blue-click-1");
});
blue.addEventListener("click", function () {
console.info("blue-click-2");
});
blue.addEventListener("click", function () {
console.info("blue-click-3");
});
</script>
</body>
</html>