前端核心知识
深入的html
提到 HTML 标签,前端工程师会非常熟悉,因为在开发页面时经常使用。但往往关注更多的是页面渲染效果及交互逻辑,也就是对用户可见可操作的部分,比如表单、菜单栏、列表、图文。
其实还有一些非常重要却容易被忽视的标签,这些标签大多数用在页面头部 head 标签内,虽然对用户不可见,但如果在某些场景下,比如交互实现、性能优化、搜索优化,合理利用它们就可以达到事半功倍的效果。
常见的meta标签
在代码开发中,推荐使用Less code,Less bug ,编写代码阅多,成本越高,维护越麻烦,健壮性也差,所以在编码时候更推荐使用
简洁代码
<meta> 标签不仅仅可以在head中声明文档类型,移动端是否禁止缩放,可以更新描述更方便搜索引擎优化
<meta> 元素可提供有关页面的元信息(meta-information),比如针对搜索引擎和更新频度的描述和关键词。
<meta> 标签位于文档的头部,不包含任何内容。<meta> 标签的属性定义了与文档相关联的名称/值对。
更可以定时刷新,重定向
例如在显示类似ppt功能时候,可以在定义5S刷新跳转下一个地址,可以使用meta,不使用js操作
<meta http-equiv="Refresh" content="5; URL=page2.html">
会在5S后自动跳转page2页面
在实际场景的大屏下,在规定时间刷新数据,可以使用meta,减少js操作onload等方法,
<meta http-equiv="Refresh" content="60">
每隔60s刷新下页面
但是标签是很方便,还是需要根据实际业务场景下使用
<progress> 可以简化进度条的书写,减少dom操作
下载进度:
<progress value="22" max="100">
</progress>
<p><b>注释:</b>Internet Explorer 9 以及更早的版本不支持 <progress> 标签。</p>
title 标签与 Hack 手段:消息提醒
在H5之前,浏览器没有闪烁图标,、弹出系统消息之类的接口,只能借助一些 Hack 的手段,比如修改 title 标签来达到类似的效果(HTML5 下可使用 Web Notifications API 弹出系统消息)。
let msgNum = 1 // 消息条数
let cnt = 0 // 计数器
const inerval = setInterval(() => {
cnt = (cnt + 1) % 2
if(msgNum===0) {
// 通过DOM修改title
document.title += `聊天页面`
clearInterval(interval)
return
}
const prefix = cnt % 2 ? `新消息(${msgNum})` : ''
document.title = `${prefix}聊天页面`
}, 1000)
效果是在浏览器标签页的消息title闪烁,这种方式也可以制作其他功能,文字滚动,或者是下载进度,当前操作是什么等等
性能优化
性能优化是前端绕不过的话题
主要性能问题基本就是两点:
- 渲染速度慢
- 请求时间长
性能优化的解决方法虽然更多涉及复杂的原因和解决方案,但是只要合理利用标签,也可以在一定程度上提高渲染速度和减少请求时间
script 标签:调整加载顺序提升渲染速度
由于浏览器的底层运行机制,渲染引擎在解析 HTML 时,若遇到 script 标签引用文件,则会暂停解析过程,同时通知网络线程加载文件,文件加载后会切换至 JavaScript引擎来执行对应代码,代码执行完成之后切换至渲染引擎继续渲染页面。
在这一过程中可以看到,页面渲染过程中包含了请求文件以及执行文件的时间,但页面的首次渲染可能并不依赖这些文件,这些请求和执行文件的动作反而延长了用户看到页面的时间,从而降低了用户体验。
在使用script标签时候,可以通过script标签的一些动态属性进行减少消耗
async 属性 | 立即请求文件,但不阻塞渲染引擎,而是文件加载完毕后阻塞渲染引擎并立即执行文件内容。 |
---|---|
defer 属性 | 立即请求文件,但不阻塞渲染引擎,等到解析完 HTML 之后再执行文件内容 |
HTML5 标准 type 属性,对应值为“module” | 让浏览器按照 ECMA Script 6 标准将文件当作模块进行解析,默认阻塞效果同 defer,也可以配合 async 在请求完成后立即执行 |
采用 3 种属性都能减少请求文件引起的阻塞时间,只有 defer 属性以及 type=“module” 情况下能保证渲染引擎的优先执行,从而减少执行文件内容消耗的时间,让用户更快地看见页面(即使这些页面内容可能并没有完全地显示)
当渲染引擎解析 HTML 遇到 script 标签引入文件时,会立即进行一次渲染。
所以这也就是为什么构建工具会把编译好的引用 JavaScript 代码的 script 标签放入到 body 标签底部,
因为当渲染引擎执行到 body 底部时会先将已解析的内容渲染出来,然后再去请求相应的 JavaScript 文件。
如果是内联脚本(即不通过 src 属性引用外部脚本文件直接在 HTML 编写 JavaScript 代码的形式),渲染引擎则不会渲染。
link 标签:通过预处理提升渲染速度
对大型单页应用进行性能优化时,也许会用到按需懒加载的方式,来加载对应的模块,但如果能合理利用 link 标签的 rel
属性值来进行预加载,就能进一步提升渲染速度。
dns-prefetch | 当 link 标签的 rel 属性值为“dns-prefetch”时,浏览器会对某个域名预先进行 DNS 解析并缓存。这样,当浏览器在请求同域名资源的时候,能省去从域名查询 IP 的过程,从而减少时间损耗。下图是淘宝网设置的 DNS 预解析 |
---|---|
preconnect | 让浏览器在一个 HTTP 请求正式发给服务器前预先执行一些操作,这包括 DNS 解析、TLS 协商、TCP 握手,通过消除往返延迟来为用户节省时间 |
prefetch/preload | 两个值都是让浏览器预先下载并缓存某个资源,但不同的是,prefetch 可能会在浏览器忙时被忽略,而 preload 则是一定会被预先下载 |
prerender | 浏览器不仅会加载资源,还会解析执行页面,进行预渲染 |
搜索优化
前端代码,除了要让浏览器更好执行,有时候也要考虑更方便其他程序(如搜索引擎)理解。合理地使用 meta 标签和 link 标签,恰好能让搜索引擎更好地理解和收录我们的页面
meta 标签:提取关键信息
网站的描述信息,这些描述信息就是通过 meta 标签专门为搜索引擎设置的,目的是方便用户预览搜索到的结果。
为了让搜索引擎更好地识别页面,除了描述信息之外还可以使用关键字,这样即使页面其他地方没有包含搜索内容,
也可以被搜索到(当然搜索引擎有自己的权重和算法,如果滥用关键字是会被降权的,
比如 Google 引擎就会对堆砌大量相同关键词的网页进行惩罚,降低它被搜索到的权重)
<meta content="拉勾,拉勾网,拉勾招聘,拉钩, 拉钩网 ,互联网招聘,拉勾互联网招聘, 移动互联网招聘, 垂直互联网招聘, 微信招聘, 微博招聘, 拉勾官网, 拉勾百科,跳槽, 高薪职位, 互联网圈子, IT招聘, 职场招聘, 猎头招聘,O2O招聘, LBS招聘, 社交招聘, 校园招聘, 校招,社会招聘,社招" name="keywords">
高效操作DOM
什么是 DOM
DOM(Document Object Model,文档对象模型)是 JavaScript 操作 HTML 的接口(这里只讨论属于前端范畴的
HTML DOM),属于前端的入门知识,同样也是核心内容,因为大部分前端功能都需要借助 DOM 来实现,比如:动态渲染列表、表格表单数据;
监听点击、提交事件;
懒加载一些脚本或样式文件;
实现动态展开树组件,表单组件级联等这类复杂的操作。
如果你查看过 DOM V3 标准,会发现包含多个内容,但归纳起来常用的主要由 3 个部分组成:
DOM 节点
DOM 事件
选择区域
选择区域的使用场景有限,一般用于富文本编辑类业务,我们不做深入讨论;DOM 事件有一定的关联性;对于 DOM节点,需与另外两个概念标签和元素进行区分:
- 标签是 HTML 的基本单位,比如 p、div、input;
- 节点是 DOM 树的基本单位,有多种类型,比如注释节点、文本节点;
- 元素是节点中的一种,与 HTML 标签相对应,比如 p 标签会对应 p 元素。
例子
代码中,“p” 是标签,
生成 DOM 树的时候会产生两个节点,
一个是元素节点 p,另一个是字符串为“啊啊啊啊啊啊啊啊”的文本节点
<p>啊啊啊啊啊啊啊啊</p>
会框架更要会DOM
框架是JS的延伸,对于vue或是react之类的框架操作dom较少,但是本质上是通过JS模拟dom的节点运行,减少对于dom的操作,但是本质还是操作DOM,
作为高级/资深前端工程师,不仅应该对 DOM 有深入的理解,还应该能够借此开发框架插件、修改框架甚至能写出自己的框架。
为什么DOM 操作耗时?
DOM 操作带来的性能问题,根本原因是浏览器的工作机制决定的
浏览器线程
浏览器包含渲染引擎(也称浏览器内核)和 JavaScript 引擎,它们都是单线程运行。
单线程的优势是开发方便,避免多线程下的死锁、竞争等问题,劣势是失去了并发能力。
**性能问题**
浏览器为了避免两个引擎同时修改页面而造成渲染结果不一致的情况,增加了另外一个机制,
这两个引擎具有互斥性,也就是说在某个时刻只有一个引擎在运行,另一个引擎会被阻塞。
操作系统在进行线程切换的时候需要保存上一个线程执行时的状态信息
并读取下一个线程的状态信息,俗称上下文切换。而这个操作相对而言是比较耗时的。
每次 DOM 操作就会引发线程的上下文切换——
从 JavaScript 引擎切换到渲染引擎执行对应操作,
然后再切换回 JavaScript 引擎继续执行,这就带来了性能损耗。
单次切换消耗的时间是非常少的,但是如果频繁地大量切换,那么就会产生性能问题。
比如多次循环递归操作DOM
循环读取一百万次 DOM 中的 body 元素的耗时是读取 JSON 对象耗时的 10 倍。
// 测试次数:一百万次
const times = 1000000
// 缓存body元素
console.time('object')
let body = document.body
// 循环赋值对象作为对照参考
for(let i=0;i<times;i++) {
let tmp = body
}
console.timeEnd('object')// object: 1.77197265625ms
console.time('dom')
// 循环读取body元素引发线程切换
for(let i=0;i<times;i++) {
let tmp = document.body
}
console.timeEnd('dom')// dom: 18.302001953125ms
虽然真实情况下不会有循环这么多次,但是如果在业务场景下会有复杂逻辑或是多个元素联动时候,就会造成非常严重的性能消耗。
例如模拟打字机效果,通过定时器或者循环打印出文字,加上和接口联调的请求数据等,如果文字特别多很容易造成闭包,内存泄漏等问题
重新渲染
另一个更加耗时的因素是元素及样式变化引起的再次渲染,在渲染过程中最耗时的两个步骤为重排(Reflow)与重绘(Repaint)。
浏览器在渲染页面时会将 HTML 和 CSS 分别解析成 DOM 树和 CSSOM 树,
然后合并进行排布,再绘制成我们可见的页面。
如果在操作 DOM 时涉及到元素、样式的修改,就会引起渲染引擎重新计算样式生成 CSSOM 树,
同时还有可能触发对元素的重新排布(简称“重排”)和重新绘制(简称“重绘”)。
可能会影响到其他元素排布的操作就会引起重排,继而引发重绘,比如:
- 修改元素边距、大小
- 添加、删除元素
- 改变窗口大小
与之相反的操作则只会引起重绘,比如:
- 设置背景图片
- 修改字体颜色
- 改变 visibility 属性值
关于重绘和重排的样式属性,可以参看这个网址:https://csstriggers.com/。
关于重排和重绘,可以通过Chrome的性能测试工具进行测试分析
给出两段代码进行分析
第一段代码,通过修改 div 元素的边距来触发重排,
渲染耗时(粗略地认为渲染耗时为紫色 Rendering 事件和绿色 Painting 事件耗时之和)3045 毫秒。
const times = 100000
let html = ''
for(let i=0;i<times;i++) {
html+= `<div>${i}</div>`
}
document.body.innerHTML += html
const divs = document.querySelectorAll('div')
Array.prototype.forEach.call(divs, (div, i) => {
div.style.margin = i % 2 ? '10px' : 0;
})
第二段代码,修改 div 元素字体颜色来触发重绘,
得到渲染耗时 2359 ms。
const times = 100000
let html = ''
for(let i=0;i<times;i++) {
html+= `<div>${i}</div>`
}
document.body.innerHTML += html
const divs = document.querySelectorAll('div')
Array.prototype.forEach.call(divs, (div, i) => {
div.style.color = i % 2 ? 'red' : 'green';
})
从两段测试代码中可以看出,重排渲染耗时明显高于重绘,同时两者的 Painting 事件耗时接近,也印证了重排会导致重绘。
怎么高效操作 DOM
知道了DOM的性能耗时问题,减少对应的操作就能提升一部分性能
在循环外操作元素
两段测试代码对比了读取 1000 次 JSON 对象以及访问 1000 次 body 元素的耗时差异,相差一个数量级。
const times = 10000;
console.time('switch')
for (let i = 0; i < times; i++) {
document.body === 1 ? console.log(1) : void 0;
}
console.timeEnd('switch') // 1.873046875ms
var body = JSON.stringify(document.body)
console.time('batch')
for (let i = 0; i < times; i++) {
body === 1 ? console.log(1) : void 0;
}
console.timeEnd('batch') // 0.846923828125ms
当然即使在循环外也要尽量减少操作元素,因为不知道他人调用你的代码时是否处于循环中
批量操作元素
比如说要创建 1 万个 div 元素,在循环中直接创建再添加到父元素上耗时会非常多。
如果采用字符串拼接的形式,先将 1 万个 div 元素的 html 字符串拼接成一个完整字符串,
然后赋值给 body 元素的 innerHTML 属性就可以明显减少耗时。
const times = 10000;
console.time('createElement')
for (let i = 0; i < times; i++) {
const div = document.createElement('div')
document.body.appendChild(div)
}
console.timeEnd('createElement')// 54.964111328125ms
console.time('innerHTML')
let html=''
for (let i = 0; i < times; i++) {
html+='<div></div>'
}
document.body.innerHTML += html // 31.919921875ms
console.timeEnd('innerHTML')
通过修改 innerHTML 来实现批量操作的方式效率很高,但它并不是万能的。
比如要在此基础上实现事件监听就会略微麻烦,只能通过事件代理或者重新选取元素再进行单独绑定。
批量操作除了用在创建元素外也可以用于修改元素属性样式
创建 2 万个 div 元素,以单节点树结构进行排布,
每个元素有一个对应的序号作为文本内容。
现在通过 style 属性对第 1 个 div 元素进行 2 万次样式调整。
下面是直接操作 style 属性的代码:
const times = 20000;
let html = ''
for (let i = 0; i < times; i++) {
html = `<div>${i}${html}</div>`
}
document.body.innerHTML += html
const div = document.querySelector('div')
for (let i = 0; i < times; i++) {
div.style.fontSize = (i % 12) + 12 + 'px'
div.style.color = i % 2 ? 'red' : 'green'
div.style.margin = (i % 12) + 12 + 'px'
}
如果将需要修改的样式属性放入 JavaScript 数组,然后对这些修改进行 reduce 操作,得到最终需要的样式之后再设置元素属性,那么性能会提升很多。
const times = 20000;
let html = ''
for (let i = 0; i < times; i++) {
html = `<div>${i}${html}</div>`
}
document.body.innerHTML += html
let queue = [] // 创建缓存样式的数组
let microTask // 执行修改样式的微任务
const st = () => {
const div = document.querySelector('div')
// 合并样式
const style = queue.reduce((acc, cur) => ({...acc, ...cur}), {})
for(let prop in style) {
div.style[prop] = style[prop]
}
queue = []
microTask = null
}
const setStyle = (style) => {
queue.push(style)
// 创建微任务
if(!microTask) microTask = Promise.resolve().then(st)
}
for (let i = 0; i < times; i++) {
const style = {
fontSize: (i % 12) + 12 + 'px',
color: i % 2 ? 'red' : 'green',
margin: (i % 12) + 12 + 'px'
}
setStyle(style)
}
缓存元素集合
比如将通过选择器函数获取到的 DOM 元素赋值给变量,之后通过变量操作而不是再次使用选择器函数来获取。
假设我们现在要将上面代码所创建的 1 万个 div 元素的文本内容进行修改。每次重复使用获取选择器函数来获取元素,代码如下所示
for (let i = 0; i < document.querySelectorAll('div').length; i++) {
document.querySelectorAll(`div`)[i].innerText = i
}
如果能够将元素集合赋值给 JavaScript 变量,每次通过变量去修改元素,那么性能将会得到不小的提升。
const divs = document.querySelectorAll('div')
for (let i = 0; i < divs.length; i++) {
divs[i].innerText = i
}
小结
从深入理解 DOM 的必要性说起,然后分析了 DOM 操作耗时的原因,最后再针对这些原因提出了可行的解决方法。
除了这些方法之外,还有一些原则也可能帮助我们提升渲染性能,比如:
- 尽量不要使用复杂的匹配规则和复杂的样式,从而减少渲染引擎计算样式规则生成 CSSOM 树的时间;
- 尽量减少重排和重绘影响的区域;
- 使用 CSS3 特性来实现动画效果。
3 个使用场景助你用好 DOM 事件
DOM 事件数量非常多,即使分类也有十多种,比如键盘事件、鼠标事件、表单事件等,而且不同事件对象属性也有差异,这带来了一定的学习难度。
但页面要与用户交互,接收用户输入,就离不开监听元素事件,所以,DOM 事件是前端工程师必须掌握的重要内容,同时也是 DOM 的重要组成部分。
从防抖、节流、代理 3 个场景出发,详细了解 DOM 事件。
防抖
场景,有一个搜索输入框,为了提升用户体验,希望在用户输入后可以立即展现搜索结果,而不是每次输入完后还要点击搜索按钮。最基本的实现方式应该很容易想到,那就是绑定 input 元素的键盘事件,然后在监听函数中发送 AJAX 请求。伪代码如下:
const ipt = document.querySelector('input')
ipt.addEventListener('input', e => {
search(e.target.value).then(resp => {
// ...
}, e => {
// ...
})
})
但其实这样的写法很容易造成性能问题。
比如当用户在搜索“lagou”这个词的时候,每一次输入都会触发搜索:
- 搜索“l”
- 搜索“la”
- 搜索“lag”
- 搜索“lago”
- 搜索“lagou”
而实际上,只有最后一次搜索结果是用户想要的,前面进行了 4 次无效查询,浪费了网络带宽和服务器资源。
所以对于这类连续触发的事件,需要添加一个**“防抖”功能**,为函数的执行设置一个合理的时间间隔,避免事件在时间间隔内频繁触发,同时又保证用户输入后能即时看到搜索结果
要实现这样一个功能我们很容易想到使用 setTimeout() 函数来让函数延迟执行。就像下面的伪代码,当每次调用函数时,先判断 timeout 实例是否存在,如果存在则销毁,然后创建一个新的定时器。
// 代码1
const ipt = document.querySelector('input')
let timeout = null
ipt.addEventListener('input', e => {
if(timeout) {
clearTimeout(timeout)
timeout = null
}
timeout = setTimeout(() => {
search(e.target.value).then(resp => {
// ...
}, e => {
// ...
})
}, 500)
})
优化
抽取成公共函数
在抽取成公共函数的同时,还需要考虑更复杂的情况:
参数和返回值如何传递?
防抖化之后的函数是否可以立即执行?
防抖化的函数是否可以手动取消?
具体代码如下,首先将原函数作为参数传入 debounce() 函数中,同时指定延迟等待时间,返回一个新的函数,这个函数包含 cancel
属性,用来取消原函数执行。flush 属性用来立即调用原函数,同时将原函数的执行结果以 Promise 的形式返回。
// 代码2
const debounce = (func, wait = 0) => {
let timeout = null
let args
function debounced(...arg) {
args = arg
if(timeout) {
clearTimeout(timeout)
timeout = null
}
// 以Promise的形式返回函数执行结果
return new Promise((res, rej) => {
timeout = setTimeout(async () => {
try {
const result = await func.apply(this, args)
res(result)
} catch(e) {
rej(e)
}
}, wait)
})
}
// 允许取消
function cancel() {
clearTimeout(timeout)
timeout = null
}
// 允许立即执行
function flush() {
cancel()
return func.apply(this, args)
}
debounced.cancel = cancel
debounced.flush = flush
return debounced
}
// 防抖处理之后的事件绑定
const ipt = document.querySelector('input')
ipt.addEventListener('input', debounce(e => {
search(e.target.value).then(resp => {
// ...
}, e => {
// ...
})
}, 500))
在写代码解决当前问题的时候,最初只能写出像代码 1 那样满足需求的代码。
但要成为高级工程师,就一定要将问题再深想一层,比如代码如何抽象成公共函数,
才能得到较为完善的代码 2,从而自身得到成长。
节流
另外一个场景,一个左右两列布局的查看文章页面,左侧为文章大纲结构,右侧为文章内容。现在需要添加一个功能,就是当用户滚动阅读右侧文章内容时,左侧大纲相对应部分高亮显示,提示用户当前阅读位置。
这个功能的实现思路比较简单,滚动前先记录大纲中各个章节的垂直距离,
然后监听 scroll 事件的滚动距离,根据距离的比较来判断需要高亮的章节。伪代码如下:
// 监听scroll事件
wrap.addEventListener('scroll', e => {
let highlightId = ''
// 遍历大纲章节位置,与滚动距离比较,得到当前高亮章节id
for (let id in offsetMap) {
if (e.target.scrollTop <= offsetMap[id].offsetTop) {
highlightId = id
break
}
}
const lastDom = document.querySelector('.highlight')
const currentElem = document.querySelector(`a[href="#${highlightId}"]`)
// 修改高亮样式
if (lastDom && lastDom.id !== highlightId) {
lastDom.classList.remove('highlight')
currentElem.classList.add('highlight')
} else {
currentElem.classList.add('highlight')
}
})
滚动事件的触发频率是很高的,持续调用判断函数很可能会影响渲染性能。实际上也不需要过于频繁地调用,因为当鼠标滚动 1 像素的时候,很有可能当前章节的阅读并没有发生变化。所以我们可以设置在指定一段时间内只调用一次函数,从而降低函数调用频率,这种方式我们称之为“节流”。
实现节流函数的过程和防抖函数有些类似,只是对于节流函数而言,有两种执行方式,在调用函数时执行最先一次调用还是最近一次调用,所以需要设置时间戳加以判断。我们可以基于 debounce() 函数加以修改,代码如下所示:
const throttle = (func, wait = 0, execFirstCall) => {
let timeout = null
let args
let firstCallTimestamp
function throttled(...arg) {
if (!firstCallTimestamp) firstCallTimestamp = new Date().getTime()
if (!execFirstCall || !args) {
console.log('set args:', arg)
args = arg
}
if (timeout) {
clearTimeout(timeout)
timeout = null
}
// 以Promise的形式返回函数执行结果
return new Promise(async(res, rej) => {
if (new Date().getTime() - firstCallTimestamp >= wait) {
try {
const result = await func.apply(this, args)
res(result)
} catch (e) {
rej(e)
} finally {
cancel()
}
} else {
timeout = setTimeout(async () => {
try {
const result = await func.apply(this, args)
res(result)
} catch (e) {
rej(e)
} finally {
cancel()
}
}, firstCallTimestamp + wait - new Date().getTime())
}
})
}
// 允许取消
function cancel() {
clearTimeout(timeout)
args = null
timeout = null
firstCallTimestamp = null
}
// 允许立即执行
function flush() {
cancel()
return func.apply(this, args)
}
throttled.cancel = cancel
throttled.flush = flush
return throttled
}
节流与防抖都是通过延迟执行,减少调用次数,来优化频繁调用函数时的性能。不同的是,对于一段时间内的频繁调用,防抖是延迟执行后一次调用,节流是延迟定时多次调用。
代理
HTML 代码是一个简单的无序列表,现在希望点击每个项目的时候调用 getInfo() 函数,当点击“编辑”时,调用一个 edit() 函数,当点击“删除”时,调用一个 del() 函数。
<ul class="list">
<li class="item" id="item1">项目1<span class="edit">编辑</span><span class="delete">删除</span></li>
<li class="item" id="item2">项目2<span class="edit">编辑</span><span class="delete" >删除</span></li>
<li class="item" id="item3">项目3<span class="edit">编辑</span><span class="delete">删除</span></li>
...
</ul>
要实现这个功能并不难,只需要对列表中每一项,分别监听 3 个元素的 click 事件即可。
但如果数据量一旦增大,事件绑定占用的内存以及执行时间将会成线性增加,而其实这些事件监听函数逻辑一致,只是参数不同而已。此时我们可以以事件代理或事件委托来进行优化。
DOM 事件的触发流程
主要分为 3 个阶段:
- 捕获,事件对象 Window 传播到目标的父对象
- 目标,事件对象到达事件对象的事件目标
- 冒泡,事件对象从目标的父节点开始传播到 Window
例如,在下面的代码中,虽然我们第二次进行事件监听时设置为捕获阶段,但点击事件时仍会按照监听顺序进行执行
<body>
<button>click</button>
</body>
<script>
document.querySelector('button').addEventListener('click', function () {
console.log('bubble')
})
document.querySelector('button').addEventListener('click', function () {
console.log('capture')
}, true)
// 执行结果
// buble
// capture
</script>
回到事件代理,事件代理的实现原理就是利用上述 DOM 事件的触发流程来对一类事件进行统一处理。比如对于上面的列表,我们在 ul 元素上绑定事件统一处理,通过得到的事件对象来获取参数,调用对应的函数。
const ul = document.querySelector('.list')
ul.addEventListener('click', e => {
const t = e.target || e.srcElement
if (t.classList.contains('item')) {
getInfo(t.id)
} else {
id = t.parentElement.id
if (t.classList.contains('edit')) {
edit(id)
} else if (t.classList.contains('delete')) {
del(id)
}
}
})
这里我们选择了默认在冒泡阶段监听事件,但和捕获阶段监听并没有区别。对于其他情况还需要具体情况具体细分析,比如有些列表项目需要在目标阶段进行一些预处理操作,那么可以选择冒泡阶段进行事件代理。