前言
事件的本质是程序各组成部分间的一种通信方式;本文主要介绍DOM事件级别,DOM事件模型,DOM事件流,以及常见应用等,希望对我们有所帮助。
一、DOM事件级别
DOM标准已有4个,其中DOM1标准没有涉及事件相关的修改
1. DOM 0级别
el.onclick = function () {...}
// html -on方式
<button class="btn0" onclick="dom0()"></button>
<script>
function dom0() {
alert('dom0')
}
</script>
// js函数式
document.querySelector('.btn0').onclick = dom0
2. DOM 2级别
el.addEventListener('evnet-name', callback, [, useCapture])*
<button class="btn2">dom2</button>
<script>
let btn2 = document.querySelector('.btn2')
btn2.addEventListener('click', function () {
alert('dom2')
})
</script>
//IE9-:attachEvent()与detachEvent()。
//IE9+/chrom/FF:addEventListener()和removeEventListener()
IE9.0以下的IE浏览器不支持
3. DOM 3级别
对DOM2进行拓展,添加更多的事件类型
- UI事件类,当用户与页面上的元素交互时触发,如:load、scroll
- 鼠标事件,当用户用鼠标在页面执行操作时触发,如:dbclick、mouseup
- 键盘事件,当用户操作键盘在页面上执行操作时触发,如:keydown、keypress
- 滚动事件,当用户滚动鼠标滚轮或类似设备时触发,如:mousewheel
- 焦点事件,当元素获取或失去焦点时触发,如:focus、blur
- 文本事件,当文档控件输入文本时触发,如:textInput
- 合成事件,当IME(输入法编辑器)输入字符时触发,如:compositionstart
- 变动事件,当底层DOM结构发生变化时触发,如:DOMsubstreeModified
- 自定义事件
小结
DOM0时期:一个事件只能监听一个函数,且只有冒泡阶段
<div onclick="console.log('div')">
<button class="btn" onclick="console.log('btn')">dom0</button>
</div>
// btn
// div
DOM2之后:可以同事监听多个函数,且支持捕获和冒泡阶段
意义:DOM2标准的出现,统一了js的监听函数接口,也日益复杂的交互做好了铺垫。
二、DOM事件模型
捕获模型和冒泡模型
三、DOM事件流
一个事件发生后,会在子元素和父元素之间传播;经历捕获、目标、冒泡阶段。
- 捕获阶段:从window对象自顶向下朝目标元素传播阶段。
- 目标阶段:在目标元素处理事件阶段。
- 冒泡阶段:从目标元素自底向上朝window对象传播阶段。
提问:事件在捕获阶段有哪些实际案例,希望有老铁可以分享?
四、Event对象常见属性与应用
事件发生以后会产生一个Event对象,其常见方法/属性如下:
- event.preventDefault()
// 阻止默认事件
// 实现一个最多输入6位数的demo
<input type="text" id="inputText">
<script>
inputText.oninput = function (e) {
const ev = e || window.event
// 去除空格
const val = this.value.replace(/^ +| +$/g, '')
const len = val.length
if(len >= 6) {
this.value = val.substr(0, 6)
let code = ev.which || ev.keyCode
// 排除DELETE/BACK/SPACE/方向键...
if (!/^(46|8|37|38|39|40)$/.test(code)) {
ev.preventDefault()
}
}
}
</script>
- event.stopPropagation()
- event.stopImmediatePropagation()
这两个都是组织冒泡;区别在于stopImmediatePropagation还能阻止别监听的其他函数不触发
// 实现一个表单验证的demo
<form action="javascript:">
<input type="text" id="name">
<input type="text" id="password">
<button id="submit">提交</button>
</form>
<script>
const name = document.getElementById('name')
const password = document.getElementById('password')
// 输入检查
submit.addEventListener('click', e => {
console.log(name.value)
if (!name.value.trim()) {
alert('you must input name')
return e.stopImmediatePropagation()
}
if (!password.value.trim()) {
alert('you must input pwd')
return e.stopImmediatePropagation()
}
}, false)
// 提交
submit.addEventListener('click', function () {
alert('done finish')
})
</script>
// 可以有效的将检查逻辑和提交代码逻辑进行解耦
提问:在目前主流框架中改如何使用,比如vue、react?
- event.currentTarget
- event.current
event.currentTarget始终是事件的监听者/event.current事件的发起者
// 事件委托/事件代理
<ul>
<li><span>1</span><span>删除</span></li>
<li><span>2</span><span>删除</span></li>
<li><span>3</span><span>删除</span></li>
<li><span>4</span><span>删除</span></li>
<li><span>5</span><span>删除</span></li>
</ul>
<script>
// 方案一:递归
const ul = document.querySelector('ul')
ul.addEventListener('click', e => {
console.log(e.currentTarget) // 始终是监听者: ul
console.log(e.target) // 事件发起者: li 或 span 或 span
e.stopPropagation()
remove(ul, e.target)
})
function remove(parent, target) {
if (target.parentElement === parent) {
parent.removeChild(target)
} else {
remove(parent, target.parentNode)
}
}
// 方案二:循环 不用担心函数调用栈溢出
const ul = document.querySelector('ul')
ul.addEventListener('click', e => {
console.log(e.currentTarget) // 始终是监听者: ul
console.log(e.target) // 事件发起者: li 或 span 或 span
e.stopPropagation()
let target = e.target
while (target.parentElement !== ul){
target = target.parentElement
}
ul.removeChild(target)
})
</script>
事件代理的优势:
- 减少内存消耗,提高性能
- 动态绑定事件;例如:当删、增dom元素时,无需再次为子元素绑定事件
提问:在目前主流框架中改如何使用,比如vue、react?
五、自定义事件
// 实现一个游戏启动场景demo
<button>开始游戏</button>
<script>
// main.js
const btn = document.querySelector('button')
btn.addEventListener('click', function() {
document.dispatchEvent(new Event('gameStart'))
console.log('开始加载图片资源...')
console.log('开始加载音频资源...')
})
// load-image.js
document.addEventListener('gameStart', function () {
setTimeout(() => {
document.dispatchEvent(new Event('imageLoaded'))
}, 500)
})
// load-music.js
document.addEventListener('gameStart', function() {
setTimeout(() => {
document.dispatchEvent(new Event('musicLoaded'))
}, 1000)
})
// init-scene.js
document.addEventListener('imageLoaded', () => {
console.log('图片加载完毕!')
console.log('创建场景...')
})
document.addEventListener('musicLoaded', () => {
console.log('音频加载完毕!')
console.log('创建音效...')
})
</script>
优势:各模块解耦,利于分工,便于扩展
缺点:要杜绝模块间的相互嵌套调用,否则定位问题难,建议由控制模块统一调度
六、函数防抖与节流
函数防抖(debounce)
n秒之后才执行;如果n秒内函数再次触发函数,则重置时间,触发时间不固定。
// 实现一个输入框检查demo
<input type="text" id="inpText">
<script>
inpText.addEventListener('keyup', check())
function check() {
let timer
return function() {
clearTimeout(timer)
timer = setTimeout(() => {
console.log('check something...')
}, 800)
}
}
</script>
函数节流(throttle)
立刻执行;如果n秒内函数再次触发函数,则不会重置时间,触发时间固定。
<section class="wrap">
<div class="content">xxx</div>
</section>
<style>
.wrap {
width: 200px;
height: 400px;
overflow: scroll;
}
.content {
height: 200%;
background: aqua;
}
</style>
<script>
const wrap = document.querySelector('.wrap')
wrap.addEventListener('scroll', throttle())
function throttle(e) {
let canUse = true
return function () {
if (!canUse) return
canUse = false
if (this.scrollTop - this.clientHeight <= 100) {
console.log('scroll to end')
}
setTimeout(() => {
canUse = true
}, 2000)
}
}
</script>
小结
都是为了解决在一定时间内频繁调用js函数的问题,一般情况都和事件相关,关于他们的封装lodash已经做得很好了。
防抖(debounce)常见的场景:连续的事件,只需触发一次回调。
- input输入表单验证
- 搜索框搜索输入;只需用户最后一次输入完,再发送请求
- 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
节流(throttle)常见的场景:间隔一段时间执行一次回调。
- 滚动加载,加载更多或滚到底部监听
- 谷歌搜索框,搜索联想功能
- 高频点击提交,表单重复提交
七、DOM事件与线程相关内容
- 待完善
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。