1. DOM
1.1 基本概念
DOM,全称 Document Object Model,即文档对象模型。它是 Web 上最常用的 API 之一,是加载在浏览器中的文档模型,可以将文档表示为节点树(或称 DOM 树),其中每个节点代表文档的一部分(例如,元素、文本字符串或注释)
它允许在浏览器中运行的代码访问文档中的每个节点并与之交互。可以创建、移动和更改节点,还可以将事件监听器添加到节点,并在特定事件发生时触发
简单来说,DOM就是JS操作网页的接口,是JS和网页之间的桥梁。
W3C官方解释:DOM是一种编程接口或平台,它允许程序或脚本(如JavaScript)动态地访问和更新文档的内容、结构和样式
1.2 DOM的作用
DOM的主要作用是将网页转为一个JS对象,从而可以用JS对网页进行各种操作,如,
1. 通过DOM,使用JS获取、修改、创建、删除文档中得元素、元素属性、文本等内容
2. 通过DOM,使用JS修改元素得样式、类名、事件处理程序等来改变页面的外观和行为
3. DOM提供了事件模型,允许在页面中注册事件监听器,并对用户的交互做出响应
1.3 DOM的形成
DOM的形成过程主要涉及到浏览器对HTML文档的解析和构建,
1. 加载HTML文档,用户输入URL向服务器请求资源,获取到HTML文档
2. 解析HTML文档,浏览器按照一定语法、规则解析HTML文档。并将其转换为一个树结构的对象,即DOM树
3. 构建DOM树,解析HTML文档的过程中,浏览器会根据文档中的元素、属性和文本等内容构建DOM树。DOM树的每个节点都代表文档中的一个元素、属性或文本等内容
4. 提供访问接口,一旦DOM树构建完成,浏览器就会提供一系列的方法和属性来访问和操作DOM树中的节点和对象
提问:为什么需要将HTML转成DOM树来提供操作网页的接口?
因为HTML文档是字符串类型的,而DOM树是可遍历的树结构对象,相比于处理超长的字符串,对象更为容易处理。
参考:浏览器渲染基本原理-CSDN博客
1.4 DOM的节点类型
DOM中总共有12种节点类型,它们都继承自一种基本类型。其中,最常用的节点类型有
1. Document节点,代表整个文档,是DOM树的根节点
2. Element节点,代表HTML文档中的标签元素,如<div>、<span>等
3. Text节点,代表元素中的文本内容
4. Comment节点,代表HTML文档中的注释内容
1.5 JS获取DOM节点
DOM是供JS操作网页元素的API,通过其提供的方法可以访问到网页元素对应的DOM树节点,
1. ID获取,这是获取单个元素节点(Element)的最快方式,因为ID在HTML文档中应该是唯一的
let element = document.getElementById('box')
2. 类名获取,这会返回一个元素节点集合,即HTMLCollection。即使只有一个元素匹配。
let elements = document.getElementsByClassName("myClassName")
// 可以通过索引访问,如elements[0]
// 也可以通过ID或name访问,如element['box'],box即元素ID
// 但第二种方法,不是所有浏览器都支持
3. 标签名获取,会返回一个元素节点集合。
let elements = document.getElementsByTagName("div")
4. name属性获取,会返回一个元素节点集合。并不常用
let element = document.getElementsByName('name')
5. css选择器获取,
①document.querySelector(),返回匹配选择器的第一个节点
let element = document.querySelector('.className')
②document.querySelectorAll(),返回所有匹配选择器的节点集合NodeList
let element = document.querySelectorAll('.className')
注意:NodeList和HTMLCollection是存在一定区别的,
区别 | NodeList | HTMLCollection |
节点类型 | 可包含几乎所有类型的节点。 如,元素节点(Element)、文本节点(Text)、注释节点(Comment)等 | 只能包含元素节点(Element) |
实时性 | 可以是动态的,也可以是静态的。 如,childNodes属性返回的NodeList是动态的; 而querySelectorAll方法返回的通常是静态的; | 通常也是动态的,当文档中的元素发生变化时,HTMLCollection会自动更新以反映文档的变化。 |
访问方式 | 通过索引来访问集合中的节点,如elements[index] | 可通过索引访问节点; 还可通过元素的name或id属性值来快速访问元素节点,注意:不是所有浏览器都支持 |
6. 事件目标获取,在事件处理函数中,可以通过事件对象(event)的target属性来获取触发事件的元素节点。
document.querySelector('.className').addEventListener('click', function (event) {let clickedElement = event.target})
7. 除了上述这些方法,DOM API还提供了document.forms和document.images等诸多方法属性来获取DOM节点。感兴趣着,建议查阅相关官方文档
1.6 JS操作DOM节点
访问到DOM节点后,我们可以使用DOM API提供的一些属性和方法来修改节点属性,以达到动态的修改网页的内容和结构的效果。下面是常用的DOM节点操作方式:
1.61 修改节点属性
1. 修改HTML内容
let element = document.querySelector('#box')
element.innerHTML = '<p>New content</p>'
2. 修改文本内容
let element = document.querySelector('#box')
element.textContent = 'New text content'
3. 修改属性值,如直接修改并覆盖该元素节点的 所有 class
属性值
let element = document.querySelector('#box')
element.setAttribute('class', 'newClassName')
1.62 样式操作
1. 修改CSS样式
let element = document.querySelector('#box')
element.style.color = 'red'
element.style.fontSize = '20px'
2. 添加/移除类
let element = document.querySelector('#box')
element.classList.add('newClassName')
element.classList.remove('newClassName')
3. 切换类
let element = document.querySelector('#box')
element.classList.toggle('className');
1.63 节点的增删
1. 创建新节点
//创建新节点
let newElement = document.createElement('div')
newElement.textContent = 'New text content'
2. 插入新节点
let element = document.querySelector('#box')//作为子节点插入
element.appendChild(newElement)
//作为兄弟节点插入,
//element.parentNode 是element节点的父节点
//insertBefore 将newElement插入到 element.nextSibling之前
//element.nextSibling 是element节点的下一个相邻节点,如果没有那就是null
element.parentNode.insertBefore(newElement, element.nextSibling)
// 所有如果element 没有下一个相邻节点,newElement会被插到parentNode的末尾
3. 删除节点
let element = document.querySelector('#box')
// 访问父节点使用removeChild删除子节点
element.parentNode.removeChild(element)
1.64 事件处理
1. 添加事件监听器
let element = document.querySelector('#box')
element.addEventListener('click', function () {alert('Element clicked!')
})
2. 移除事件监听器
let element = document.querySelector('#box')
function handleClick() {alert('Element clicked!')
}
element.addEventListener('click', handleClick)
element.removeEventListener('click', handleClick)
1.65 综合案例
以下是一个综合示例,展示了如何动态创建一个列表,并添加点击事件来处理列表项的点击:
<div id="container"></div>
<script>// 获取容器 元素节点let container = document.getElementById('container')// 创建列表项for (let i = 1; i <= 5; i++) {let listItem = document.createElement('div')listItem.textContent = 'Item ' + ilistItem.className = 'list-item'// 添加点击事件监听器listItem.addEventListener('click', function () {alert('You clicked on: ' + this.textContent)})// 将列表项作为容器子元素添加container.appendChild(listItem)}
</script>
2. 事件
2.1 基本概念
早期的互联网受限于网络带宽和服务器处理能力,网页的响应速度往往非常慢。为了提高用户体验,开发人员尝试将原本需要在服务器端处理的任务部分前移到客户端,让客户端通过JS来实现,以减少对服务器的依赖。如表单信息的格式验证等。
随着这种客户端处理策略的发展,浏览器开始支持越来越多的事件类型。
事件就是用户和网页交互时,由浏览器或渲染引擎根据用户的操作(如点击、输入、移动鼠标等)或某些系统事件(如网页加载、调整窗口的等)触发的行为。
这些行为通常与函数结合使用,当触发这些行为时,浏览器就会执行这些 “事件处理函数” 来响应事件,由此我们可以概括出:事件就是一种 “ 触发——响应 ”的机制。
同时,一个完整的事件机制必须满足三个要素(举例说明):
<button id="btu">btu</button>
let btu = window.document.querySelector('#btu')
btu.addEventListener('click', function () {console.log('触发事件处理函数')// ..........
})
- 事件源(EventTarget),也就是触发对象,比如上面的button
- 事件类型(type),比如上面的单击事件click
- 事件处理程序,比如上面的function
事件和DOM的关系:
- 由用户触发的事件,通常是注册在DOM节点上的,所以本质上,这类事件就是由DOM产生的资源。
- 而由系统触发的事件,如resize这种窗口大小改变时触发的事件,通常是注册在window对象上的。注意:DOM节点被包含在document内,而,document被包含在window对象内。
2.2 事件绑定
1. 在HTML/XML标签中直接绑定。通常用于简单的场景,如onclick、onchange等,并直接指定处理函数名称
<button onclick="alert('你单击了一次')">按钮</button>
2. 在JS代码中给DOM绑定,通过JS访问DOM节点,并为其设置事件处理函数。优点是与HTML标签的分离,使文档结构更加清晰,便于管理和开发。
function handleClick() {alert('单击了一下')
}
document.getElementById('myButton').onclick = handleClick
3. 使用事件监听函数注册事件。它提供了更灵活的事件处理机制,能够为多个对象注册相同的事件处理函数,也可以为同一个对象注册多个事件处理函数,还可以在捕获阶段或冒泡阶段(详情见下方事件流)处理事件,并且可以绑定多个事件处理函数。
// 语法:
element.addEventListener(String type, Function listener, boolean useCapture)
type:事件类型
listener:事件处理函数
useCapture:true,指定的listener在捕获阶段触发;false,指定listener在冒泡阶段触发
-----------------------------------------------------------------------
document.getElementById('myButton').addEventListener('click',function () {alert('单击了一下')},false
)
4. 事件解绑
即移除绑定的事件处理函数,防止事件被重复触发或造成内存泄漏等问题。事件解绑通常与事件绑定相对应,如通过addEventListener注册的事件可以用removeEventListener来移除对应的侦听器
let btu1 = document.getElementById('btu1')
//单击一次,触发处理函数后解绑该处理函数,注意:处理函数不能匿名
btu1.addEventListener('click', function ok() {alert('单击了一下')this.removeEventListener('click', ok, false)
})//当临时注册一个事件时,可以在处理完毕之后迅速删除它,这样能够节省系统资源
注意:事件的处理函数最好不要使用箭头函数 ,否则处理函数内this不会指向源对象
2.3 事件流
1. 基本概念
事件流就是多个节点对象同一事件进行相应的先后顺序,一般有冒泡和捕获两种模式。通常指由于嵌套元素节点注册了相同的事件,并由子元素触发时,引起其上级元素触发相同事件
<div id="box"><button id="btu">btu</button>
</div>
<script>let btu = document.querySelector('#btu')let box = document.querySelector('#box')function check(e) {// e.target就是触发该处理函数的节点console.log('trigger by', e.target, 'and element is', this)}btu.addEventListener('click', check)box.addEventListener('click', check)
</script>
当我们点击button时,控制台输出:
会发现,由button触发的事件冒泡到了它的上级元素节点上
2. 事件冒泡
是事件流的默认模式,在DOM结构中,当一个元素被触发事件时,该事件会沿着DOM树向上传播,直到根节点或遇到某个节点阻止了事件的传播。这种传播方式称为事件冒泡。如 "1.基本概念" 中所展示的情况。
2. 事件捕获
与事件冒泡相反,事件捕获是事件从根节点向目标元素传播的过程。在事件捕获阶段,事件会先触发祖先元素的事件处理程序,然后再触发目标元素的事件处理程序。如下:
我们将 "1.基本概念" 中的box注册的单击事件,由默认的冒泡模式,改为捕获模式
box.addEventListener('click', check, true)
此时,再点击button,控制台会输出
会发现,由button触发的单击事件,却是button的上级元素box先一步调用单击事件处理函数。
如果不好理解,那么接着往下看
3. 事件处理顺序
事件由某一DOM节点触发,经历下面三个阶段
① 捕获阶段:事件从文档的根节点document向目标节点传播,如果绑定了捕获阶段的事件处理函数,则在这个阶段执行。
② 目标阶段:事件到达目标节点,如果为目标节点绑定了事件处理函数,则在这个阶段执行。
③ 冒泡阶段:事件从目标节点向文档的根节点传播,如果绑定了冒泡阶段的事件处理函数,则在这个阶段执行。
所以,"1.基本概念" 中的事件顺序应该是,
1)btu节点触发了单击事件click,浏览器从根节点document开始向btu节点遍历DOM树,发现了btu的上级元素box节点 也注册了单击事件,但默认了在冒泡阶段触发,所以不执行处理函数check
2)浏览器找到btu节点,开始触发处理函数
3)浏览器从btu节点开始向根节点document遍历,并执行box节点 注册的冒泡阶段单击事件(即执行check函数)
4. 事件混合
一句话概括,多重嵌套元素节点注册相同事件并分别指定了在冒泡阶段或捕获阶段执行
2.4 事件委托
事件委托是一种利用事件冒泡原理的技术,它通过将事件监听器绑定到父元素或祖先元素上,而不是直接绑定到目标元素上,来实现对多个子元素的事件处理。这种方式可以减少事件监听器的数量,提高性能,并简化代码结构。
2.5 阻止事件传播
即阻止事件冒泡/或捕获阶段的事件传播。某些场景我们不希望子元素节点触发的事件会传播到上级元素节点,这可以通过以下几种方式实现:
1. stopPropagation()方法,阻止捕获和冒泡阶段中当前事件的进一步传播(即传播到父元素)。这个方法只会阻止事件的冒泡阶段,不会阻止捕获阶段(除非在捕获阶段调用)
<div id="box"><button id="btu">btu</button>
</div>
<script>let btu = document.querySelector('#btu')let box = document.querySelector('#box')function check() {event.stopPropagation()console.log('trigger by', event.target, 'and element is', this)}btu.addEventListener('click', check)//捕获阶段调用box.addEventListener('click', check, true)// 冒泡阶段调用box.addEventListener('click', function () {console.log(this)})
</script>
点击点击button只会触发btu的处理函数,控制台输出:
box注册在冒泡阶段触发的处理函数,被btu注册的处理函数阻止事件传播到上级元
box注册在捕获阶段触发的处理函数,被box自己注册的处理函数阻止事件传播到这里
注意:stopPropagation()方法不能防止任何默认行为的发生;例如,对链接的点击仍会被处理。
2. preventDefault()方法,阻止事件的默认行为。但此事件还是继续传播。它通常用于阻止表单提交submit的默认行为
<form id="myForm"><label for="username">Username:</label><input type="text" id="username" name="username" required /><button type="submit">Submit</button>
</form>let form = document.getElementById('myForm')
form.addEventListener('submit', function (event) {// 阻止表单的默认提交行为event.preventDefault()// ......在这里执行自定义逻辑,比如验证表单字段
})
因为JS表单的submit提交事件,默认会直接向服务器直接提交数据,但通常我们需要在提交之前对表单数据进行简单的格式验证,当格式正确后再发送到服务器。
3. event.stopImmediatePropagation(),不仅阻止事件的进一步传播,还阻止监听同一事件的其他事件监听器被调用
<button id="btu"></button>
<script>const btu = document.querySelector('#btu')btu.addEventListener('click', function () {console.log('button注册的第一个监听函数')})btu.addEventListener('click', function () {console.log('button注册的第二个监听函数')// // 阻止事件冒泡,并且阻止btu上绑定的其他 click 事件的事件监听函数的执行。event.stopImmediatePropagation()})btu.addEventListener('click', function () {console.log('button注册的第三个监听函数')// 该监听函数在stopImmediatePropagation后,故不会被执行})document.addEventListener('click', function () {console.log('给根节点注册的click监听函数')//btu 的 click 事件没有向上冒泡,该函数不会被执行})
</script>
注意:stopImmediatePropagation的阻止事件进一步传播是按照事件处理顺序来阻止的。
如果将给根节点注册的click事件处理函数设置为在捕获阶段执行,那么单击button,依旧会执行该处理函数。
document.addEventListener('click',function () {console.log('给根节点注册的click监听函数')//该函数会顺利执行,应为事件处理顺序在stopImmediatePropagation之前//....//如果在捕获阶段使用stopImmediatePropagation阻止事件传播//那么后续btu上注册的都不会执行},true)
2.6 事件类型
常见的事件类型包括:
事件类型 | 事件名称 | 触发时机 | 作用举例 |
鼠标事件 | click | 用户单击元素触发 | 常用于按钮点击、链接跳转等场景 |
dblclick | 双击元素时触发 | 常用于打开文件、放大图片等操作 | |
mouseover | 当鼠标指针移动到元素上方时触发 | 常用于显示悬停效果、提示信息等 | |
mouseout | 当鼠标指针移出元素时触发 | 常用于处理元素移出时的清理操作等 | |
mousemove | 当鼠标指针在元素上移动时持续触发 | 常用于拖拽操作、鼠标轨迹跟踪等 | |
mousedown | 当按下鼠标按钮时触发 | 常用于处理按钮点击的开始阶段 | |
mouseup | 当释放鼠标按钮时触发 | 常用于处理按钮点击的结束阶段 | |
键盘事件 | keydown | 按下键盘上的任意键时触发 | 常用于实现键盘快捷键、搜索框自动完成等功能 |
keyup | 释放键盘上的任意键时触发 | 常用于检测按键释放后的操作 | |
表单事件 | submit | 当表单提交时触发 | 常用于验证表单数据、阻止默认提交行为、异步提交等 |
change | 当表单元素的值发生变化时触发(如输入框、选择框的值改变) | 常用于实时搜索、过滤数据、更新页面内容等 | |
focus | 当元素获得焦点时触发(如输入框被点击或Tab键选中) | 常用于改变输入框的样式、显示/隐藏帮助文本或提示信息等 | |
blur | 当元素失去焦点时触发 | 常用于处理元素失去焦点后的操作,如验证输入内容 | |
窗口事件 | load | 当页面或资源(如图片、CSS文件等)完全加载完成时触发 | 常用于初始化页面内容、加载数据等 |
unload | 当页面卸载时触发,通常用于清理资源或执行最后的操作 | 常用于清理资源、保存状态等 | |
resize | 当浏览器窗口大小改变时触发 | 常用于响应式设计、调整页面布局等 | |
scroll | 当页面滚动时触发 | 常用于创建滚动效果、懒加载图片等 |
注意: 如果需要在事件监听器addEventListener以外的地方使用,如HTML中和JS赋值绑定中(2.2事件绑定中的1和2),需要使用on<event>的格式来声明事件
2.7 自定义事件
2.71 基本概念
JS内置的DOM事件通常是基于Event接口或其子类实现的。Event接口表示在EventTarget(如元素,文档,窗口等)上出现的事件。除了这些内置事件(如鼠标事件、键盘事件等),我们也可以通过事件接口(或者说构造函数)来自定事件。
2.72 Event
1. Event构造函数,用于创建一个新的事件对象 Event。如,
let event = new Event(type, options)type ,事件类型,如 "click" 这样的的字符串
options(可选) ,接受以下字段bubbles(可选),默认值为 false,表示该事件是否冒泡。cancelable(可选),默认值为 false,表示该事件能否被取消,即是否可以调用 preventDefault() 来阻止默认行为(注意:别与移除事件监听器混淆)composed(可选),默认值为 false,指示事件是否会在影子 DOM 根节点之外触发侦听器,本篇不作详细介绍
使用举例,创建一个支持冒泡且不能被取消的 look 事件。
//创建新的事件
const ev = new Event('look', { bubbles: true, cancelable: false })
// 监听自定义事件
btu.addEventListener('look', function () {console.log('btu触发look')
})
document.addEventListener('look', function () {console.log('look冒泡到document')
})
btu.addEventListener('click', function () {btu.dispatchEvent(ev)//自定义的事件使用dispatchEvent来触发
})
//点击button,顺序输出:btu触发look look冒泡到document
2. Event构造函数作为JS内置对象的一种,提供了许多事件相关的属性和方法。而事件对象就利用这是实例属性和方法来储存或操作事件相关的信息(包括本次触发事件的事件类型、事件目标、触发元素等)。
我们查看1中定义的初始ev对象,以及btu触发look事件后的ev对象
console.dir(ev)
btu.addEventListener('look', function () {console.dir(ev)console.log(ev === event)//true,同一引用
})
控制台:
2.72 event的常用属性/方法
1. 常用属性
属性 | 介绍 |
type | 事件类型, |
bubbles | Event实例只读属性,表示当前事件是否会进行冒泡。只能在event创建时设置 |
cancelable | Event实例只读属性,表示当前事件是否能取消。只能在event创建时设置 |
isTrusted | 只读属性(无法修改),自定义事件或被重定义的事件为false,内置事件为true |
target | 事件最初发生的元素,用以确定用户与哪个元素进行了交互 |
currentTarget | 当前事件监听器所绑定的元素,即使事件是从子元素触发的,currentTarget始终指向绑定事件处理函数的元素 |
clientX 和 clientY | 获取鼠标相对于浏览器窗口左上角的坐标。常用于确定鼠标在窗口中的位置 |
key | 在键盘事件中,key属性返回按下的具体字符 |
button | 在鼠标事件中,表示按下了哪个鼠标按钮。0(左键)、1(滚轮)、2(右键) |
key | 在键盘事件中,key属性返回按下的具体字符 |
defaultPrevented | 只读属性,示当前事件是否调用了preventDefault()方法,从而阻止了浏览器的默认行为。 |
2. 常用方法
方法 | 介绍 |
preventDefault() | 阻止事件的默认行为。例如,点击链接时,浏览器会跳转到链接地址,使用preventDefault()可以阻止这一行为 |
stopPropagation() | 阻止事件冒泡,即阻止事件从子元素向父元素传递 |
stopImmediatePropagation() | 除了阻止事件冒泡外,还会阻止当前元素上其他同类型事件处理程序的执行 |
2.73 特殊的自定义事件
观察“2.72 event的常用属性/方法” 和 “2.72 Event 中对ev对象的检查 ”,会发现,某些属性在ev上并不存在。这是为什么呢?
在 “2.71基本概念” 中提到 JS内置的DOM事件通常是基于Event接口或其子类实现的。Event是基层事件接口,而它的子类接口继承了Event的部分属性,同时新增了自己持有的某属性或方法。
如键盘事件,如果我们要自定义一个键盘事件KeyboardEvent构造函数:
<input type="text" id="myInput" placeholder="在这里输入..." />
<p id="output"></p>
<script>
//DOMContentLoaded事件,文档被加载和解析,并且 DOM 被完全构造时触发
//但链接的资源(例如图像、样式表和子框架)可能尚未被加载document.addEventListener('DOMContentLoaded', (event) => {const input = document.getElementById('myInput')const output = document.getElementById('output')// 监听输入框的键盘事件input.addEventListener('keydown', (event) => {output.textContent = `你按下了键: ${event.key}`})// 创建一个自定义的键盘事件function triggerCustomKeyEvent(key) {const event = new KeyboardEvent('keydown', {key: key, // 按键的值(例如 'a', 'Enter' 等)code: key, // 物理按键的代码bubbles: true, // 冒泡cancelable: true // 是否可以调用 preventDefault() 来阻止默认行为})// 触发自定义事件input.dispatchEvent(event)}// 在页面加载后的2秒钟触发一个自定义的 'a' 键按下事件setTimeout(() => {triggerCustomKeyEvent('a') // 'a'}, 2000)})
</script>
所以,如果你想定义一些特别的自定义事件,键盘事件,鼠标事件等,就需要使用KeyboardEvent,MouseEvent 等Event的子类构造函数来实现。
对Event的子类有过一点了解后,我们对2.72Event自定义事件进行补充:
对于我们自己的全新事件类型,更加推荐使用Event的子类CustomEvent,它与Event技术上基本一样,但在第二个参数(options)中,我们可以为添加一个初始化事件时传递的任何信息:
<h1 id="elem">Hello for John!</h1>
<script>// 事件附带给处理程序的其他详细信息elem.addEventListener('hello', function (event) {alert(event.detail.name)})elem.dispatchEvent(new CustomEvent('hello', {detail: { name: 'John' }}))
</script>
请注意,随着JavaScript标准的不断发展,新的事件属性和方法可能会被引入,而一些旧的或不再常用的可能会被废弃或移除。因此,对自定义事件相关感兴趣者,建议查阅最新的ECMAScript规范或JavaScript文档以获取最准确的信息。
若有错误或描述不当的地方,烦请评论或私信指正,万分感谢 😃