UI、表达控件、加载资源、杂项
3 UI 事件
3.1 鼠标事件
该事件不仅来自鼠标,也可能是其他兼容性设备模拟鼠标操作(平板、手机)。
3.1.1 常见鼠标事件
鼠标:
mousedown/mouseup: 在元素上点击 / 释放。mouseover/mouseout: 从一个元素上移入 / 移出。mousemove: 在元素上的移动就会触发。click: 鼠标左键触发。在发生mousedown及mouseup这两个事件后,会触发该事件。dblclick: 在短时间内双击同一元素后触发,很少使用。contextmenu: 鼠标右键按下时触发。- 还有其他打开菜单的方式。比如特定的键盘按键也会触发,因此它不完全是鼠标事件。
3.1.2 事件顺序
鼠标事件的触发之间是有先后顺序的,比如:
- 一次左键单击事件:
mousedown-->mouseup-->click - 一次左键双击事件:
mousedown-->mouseup-->click-->mousedown-->mouseup-->click-->dblclick
3.1.3 事件属性 - 鼠标按钮
点击事件(mousedown, mouseup, click, dblclick, contextmenu)都会拥有一个 event.button 属性,用来保存触发事件的鼠标按键状态:
| 鼠标按键状态 | event.button |
|---|---|
| 左键 (主要按键) | 0 (常见) |
| 中键 (辅助按键) | 1 |
| 右键 (次要按键) | 2 (常见) |
| X1 键 (后退按键) | 3 |
| X2 键 (前进按键) | 4 |
3.1.4 事件属性 - 组合键
鼠标事件包含了组合键信息,以下是事件属性。如果在事件时,按下了相应的按键,则对应会置为 true。
event.shiftKey:Shift;event.altKey:Alt(或对于 Mac 是 Opt);event.ctrlKey:Ctrl;event.metaKey:对于 Mac 是 Cmd。
注意:在 Mac 上,通常使用 cmd 代替 ctrl。所以,在判断用户是否按下 ctrl 组合键时,要这样检查:
if (event.ctrlkey || event.metakey)
3.1.5 事件属性 - 坐标
所有的鼠标事件都提供了两种形式的坐标:
- 相对于视口的坐标:
clientX和clientY。 - 相对于文档的坐标:
pageX和pageY。
3.1.6 干扰
鼠标事件有事会有副作用,在某些界面中可能会出现干扰:
- 双击事件:比如双击一个文本,除了会触发我们设定的
dblclick事件外,还会选择文本。 - 按下鼠标:在按下鼠标左键,不松开的情况下拖动鼠标,也会触发选中文本。
解决方案,阻止 mousedown 事件中,浏览器的默认行为:
- 使用
return false:<b ondblclick="alert('Click!')" onmousedown="return false">XXXX</b>
3.1.6.1 防止复制
额外的tips,如何防止浏览器中,用户的复制行为,保护文本不被复制:
<div oncopy="alert('不允许复制!'); return false">
这里是不允许复制的文本内容。
</div>
使用 oncopy 特性,返回 false,在用户尝试右键点击复制的时候,就会触发 oncopy 中的代码,弹出提示框,最终会失败。
3.2 移动鼠标
mousedown/mouseup: 在元素上点击 / 释放。mouseover/mouseout: 从一个元素上移入 / 移出。
3.2.1 事件属性 - event.relatedTarget
relatedTarget属性是对target的补充。relatedTarget的值可以为null,表明可能是鼠标从另一个窗口过来(over)、或移动到了另一个窗口上(out)。
当鼠标从 A 元素离开,已经移动到了 B 元素时:
对于
mouseover:event.target:鼠标移到的当前元素 —— B 元素。event.relatedTarget:鼠标之前所处的元素 —— A 元素。对于
mouseout,与 over 相反,记住 over 就行:event.target:鼠标之前所处的元素 —— A元素。event.relatedTarget:鼠标移到的当前元素 —— B 元素。
记: target 属性是我们的主要目的,relatedTarget 属性是我们为了方便而增添的附加信息。
- 所以,对于
mouseover我们主要关注的是也就是当前鼠标所处的位置(over),这个值自然是保存到target中。
3.2.3 元素的跳过
mousemove事件,是随着鼠标的移动而触发。浏览器会间隔很小的周期,不断的重复检查鼠标的坐标位置,用以确定是否触发 mousemove 事件。
- 通过
mousemove事件,浏览器就可以计算出mouseover事件;通过mouseover事件,浏览器就可以监听到mouseout事件。
这意味着,当鼠标移动的速度非常快,可能在这个“小的周期”中,鼠标一下划过过了多个元素,这就会导致浏览器没有及时检测到鼠标具体划过了哪几个元素,造成了元素的跳过。
如果鼠标从上图所示的
#FROM快速移动到#TO元素,则中间的<div>元素可能会被跳过。mouseout事件可能会在#FROM上被触发,然后立即在#TO上触发mouseover。如果
mouseover被触发了,则mouseout也一定会触发,这两者是一一对应的。- 如果鼠标指针“正式地”进入了一个元素(生成了
mouseover事件),那么一旦它离开,我们就会得到mouseout。
- 如果鼠标指针“正式地”进入了一个元素(生成了
3.2.4 mouseover 的细节
先说原则:
- 鼠标指针移动到嵌套最多的那个元素上,也就是视觉上最突出的那个元素上(z-index最大的那个),就会触发
mouseover事件。 - 可以非常笼统的说,在视觉上分割出的区域(子元素和父元素在视觉上是两个区域),鼠标在这两个区域移动,就会触发 over,out
以下分两种情况讨论:
3.2.4.1 父元素 ==> 子元素
当鼠标从父元素移动到子元素时,在父元素上就会触发 mouseout 事件,在子元素上就会触发 mouseover 事件。
- 如果设置了事件会发生捕获,则子元素上如果设置了
mouseover事件,也会被触发。
3.2.4.2 子元素 ==> 父元素
当鼠标从子元素移动到父元素是,在子元素上就会触发 mouseout 事件,在父元素上就会触发 mouseover 事件。
- 与此同时,由于默认情况下事件会冒泡。因此,如果父元素上设置了
mouseout的事件处理程序,也会触发mouseout的回调。- 注意:此时触发的
mouseout是归属于子元素的,所以虽然因冒泡在父元素上也触发了该事件,但其属性event.target的值,依然同子元素上完全相同。
- 注意:此时触发的
因此,如果要判断鼠标是否离开了父元素和其嵌套的子元素,不能单单判断父元素上是否触发了 mouseout,而是要具体判断:
event.target的值是不是父元素。如果是,才能证明触发事件的元素,就是父元素本身。或,
event.relatedTarget的值是不是子元素。如果是,证明鼠标是从子元素移动到父元素上,而不是从外部移动到父元素。或,
mouseenter和mouseleave事件。
3.2.5 mouseenter 和 mouseleave
事件 mouseenter/mouseleave 类似于 mouseover/mouseout。它们在鼠标指针进入/离开元素时触发。
但是有两个重要的区别:
- enter 和 leave 事件,元素内部与后代之间的转换不会产生影响。
- 同时,事件
mouseenter/mouseleave不会冒泡。
当鼠标指针进入一个元素时,会触发 mouseenter,当鼠标指针离开该元素时,事件 mouseleave 才会触发。
- 与 over/out 的显著区别,就是没有了子元素嵌套的概念。只要还处在父元素中,即便是进入了更深的子元素,也依然不会触发
mouseleave直到完全离开的父元素,才会触发。
3.2.6 事件委托
利用 mouseover 和 mouseout 可以建立事件委托,简单的例子如下:
在列表的 <ul> 上设置 mouseover 监听,利用对 event.target 属性值,可以判断出当前鼠标在其子元素中的哪一个位置。
<ul id="test">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
let ul = document.querySelector('#test');
ul.onmouseover = function(event) {
let text = event.target.firstChild; // 获取li标签中包含的文本值
console.log(text); // 当鼠标移动到某个li中,就会监听到,然后在控制台输出文本值:"1", "2"或"3"。
}
</script>
相反,mouseenter 和 mouseleave 由于忽略了父子元素的关系,不可以使用事件委托来监听。
3.3 拖放鼠标
3.3.1 算法
鼠标的拖放,简单来说就是三个步骤:鼠标按下、鼠标拖动、鼠标释放,对应了三个事件监听:mousedown, mousemove, mouseup。
基础的拖放算法,在触发相关事件时,通常要做出如下行为:
mousedown: 设置好准备移动的元素,可能是创建一个副本,也可能是设置他的position: absolute。mousemove:通过更改position:absolute情况下的left/top来移动它。mouseup:执行与完成的拖放相关的所有行为。
有好几个应用,值得[反复记忆](鼠标拖放事件 (javascript.info))。
3.4 指针事件
3.4.1 历史
很早以前,只有鼠标事件。
引入了触摸事件。有了手机和平板电脑,触摸设备比鼠标具有更多的功能。例如,多点触控。鼠标事件并没有相关属性来处理这种多点触控。
例如
touchstart、touchend和touchmove,它们具有特定于触摸的属性(这里不再赘述这些特性,因为指针事件更加完善)。不过这还是不够完美。很多输入设备(如触控笔)都有自己的特性。而且同时维护鼠标事件和触摸事件的代码,非常笨重。
引入了全新的规范「指针事件」。为各种指针输入设备提供了一套统一的事件。
注: IE 10 或 Safari 12 或更低的版本不兼容指针事件。
3.4.2 指针事件类型
指针事件的命名方式和鼠标事件类似:
| 指针事件 | 类似的鼠标事件 |
|---|---|
pointerdown | mousedown |
pointerup | mouseup |
pointermove | mousemove |
pointerover | mouseover |
pointerout | mouseout |
pointerenter | mouseenter |
pointerleave | mouseleave |
pointercancel | - |
gotpointercapture | - |
lostpointercapture | - |
3.4.3 指针事件属性
指针事件具备和鼠标事件完全相同的属性,包括 clientX/Y 和 target 等。
以及一些其他属性:
pointerId:触发当前事件的指针唯一标识符。浏览器生成的,解决多指针同时触发的问题。pointerType:指针的设备类型,必须为字符串。可以是:“mouse”、“pen” 或 “touch”。- 我们可以针对不同类型的指针输入做出不同响应。
isPrimary:当指针为首要指针(多点触控时按下的第一根手指)时为true。
有些指针设备会测量接触面积和点按压力(指压在触屏上),有很少使用的属性配合:
width:指针(例如手指)接触设备的区域的宽度。对于不支持的设备(如鼠标),这个值总是1。height:指针(例如手指)接触设备的区域的长度。对于不支持的设备,这个值总是1。pressure:触摸压力,一个介于 0 到 1 之间的浮点数。对于不支持的设备,这个值总是0.5(按下时)或0(未按下时)。tangentialPressure:归一化后的切向压力(tangential pressure)。tiltX,tiltY,twist:针对触摸笔的几个属性,用于描述笔和屏幕表面的相对位置。
3.4.4 多点触控
我们可以通过 pointerId 和 isPrimary 属性的帮助,处理多点触控。
当用户用一根手指触摸在触摸屏的某个位置,然后将另一根手指放在该触摸屏的其他位置时,会发生以下情况:
- 第一个手指触摸:
pointerdown事件触发,isPrimary=true,并且被指派了一个pointerId。
- 第二个和后续的更多个手指触摸(假设第一个手指仍在触摸):
pointerdown事件触发,isPrimary=false,并且每一个触摸都被指派了不同的pointerId。
最终,如果有五个手指放在了屏幕上,我们会得到 5 个pointerdown 事件,和 5 个pointerId。
3.4.1 指针中断 - pointercancel
pointercancel 事件在触发后,会取消当前处在活跃状态的指针。该事件常常用在主动中断指针,使被中断的指针不会继续触发其他指针事件:
导致指针中断的可能原因如下:
- 指针设备硬件在物理层面上被禁用。
- 设备方向旋转(例如给平板转了个方向)。
- 浏览器开始处理这一交互。比如将其看作是一个专门的鼠标手势或缩放操作等。
- 通常,一个对物体的拖拽操作,浏览器就会接管,主动触发
pointercancel事件。 - 我们可以通过阻止浏览器默认行为,来防止
pointercancel事件的触发。
- 通常,一个对物体的拖拽操作,浏览器就会接管,主动触发
如何阻止阻止浏览器默认行为,来防止 pointercancel 事件的触发:
- 阻止原生的拖放操作发生:
- JS 中设置:
someElement.ondragstart = () => false,也适用于鼠标事件。
- JS 中设置:
- 阻止其他触摸相关的浏览器默认操作:
- CSS 中设置:
#someElement { touch-action: none }来阻止它们。
- CSS 中设置:
1.1.2 指针捕获 - setPointerCapture()
指针捕获允许一个特定的指针事件(PointerEvent) 事件从一个事件触发时候的目标重定位到另一个目标上。这个功能可以确保一个元素可以持续的接收到一个pointer事件,即使这个事件的触发点已经移出了这个元素(比如,在滚动的时候)。
比如,在设置拖动一个小方块(box)的时候,指针事件在 document 上监听,一旦监听到指针处在 box 上时,可以使用指针捕获 (setPointerCapture) 把 event.target 重定向(指向)到 box 上,这样的好处有:
- 其他元素将不能再作为该 pointer 事件的目标了,其他元素的
pointerover,pointeroutpointerenter, 和pointerleave事件将不会被触发。接下来所有的指针事件,都会被重定向到 box 上。 - 确保 box 可以持续的接收到一个pointer事件,即使这个事件的触发点已经移出了这个元素。 比如在拖动划动条,鼠标经常会离开划动块儿的区域。利用指针捕获可以确保指向 box 的 pointer 事件一直在活跃状态。
- 即使用户在整个文档上移动指针,事件处理程序也将仅在
thumb上被调用。 此外,事件对象的坐标属性,例如clientX/clientY仍将是正确的,捕获仅影响target/currentTarget。
语法:
elem.setPointerCapture(pointerId) :指针捕获。
- 将给定的
pointerId绑定到elem。 在调用之后,所有具有相同pointerId的指针事件,都将elem作为目标(就像事件发生在elem上一样),无论elem在文档中的实际位置是什么。
elem.releasePointerCapture(pointerId):取消指针捕获。
绑定会在以下情况下被移除:
- 当
pointerup或pointercancel事件出现时; - 当
elem被从文档中移除后; - 当
elem.releasePointerCapture(pointerId)被调用后。
3.5 键盘事件
keydown 事件:当一个按键被按下时触发;
keyup 事件:当一个按键被释放时触发。
3.5.1 事件对象
event.key 属性:获取当前按键的字符,会受大小写 (shift) 的影响而保存不同字母。
event.code 属性:获取当前按键的“物理按键代码”。和按键一一对应,不会改变。
- 区分,
event.code准确地标明了哪个键被按下。如两个 Shift 键,会区分"ShiftRight","ShiftLeft"。event.key只标明按键的“含义”,即它是什么(一个“Shift”),随着OS不同会因此改变:cmd。
比如,按键 “Z” 的效果:
| Key | event.key | event.code |
|---|---|---|
| Z | z(小写) | KeyZ |
| Shift+Z | Z(大写) | KeyZ |
更多举例:
| Key | event.key | event.code |
|---|---|---|
| F1 | F1 | F1 |
| Backspace | Backspace | Backspace |
| Shift | Shift | ShiftRight 或 ShiftLeft |
event.code 按键代码:
字符键:
"Key<letter>":"KeyA","KeyB"等。数字键:
"Digit<number>":"Digit0","Digit1"等。- 特殊按键,为按键的名字:
"Enter","Backspace","Tab","ShiftLeft"等。
- 特殊按键,为按键的名字:
更多:UI 事件代码规范 。
3.5.2 兼容性问题
event.key 会受到不同OS平台的影响,而呈现不同的效果。例如在使用“撤销”组合按下时:
- MacOS:是
Cmd + Z。 - Windows:是
Ctrl + Z。
event.code 会受到不同键盘布局的影响,相同的按键位置却收到不同的结果,同样在“撤销”组合按下时:
美式布局 (QWERTY):是正常的,按下 Z 时,
event.code等于KeyZ。德式布局 (QWERTZ):按下 Y 时,
event.code也等于KeyZ。
因此,event.code 可能由于特殊键盘布局,会错误的匹配字符。幸运的是,这种情况只发生在几个代码上,例如 keyA,keyQ,keyZ,可以在 规范 中找到该列表。
总结:
如果频繁切换语言(德式键盘、美式键盘),使用
event.key更好;如果想兼容更多操作系统(MacOS、Win),使用
event.code更好。
3.5.3 自动重复
触发自动重复, event.repeat 属性会被设置为 true。
如果按下一个键足够长的时间,它就会开始“自动重复”:
keydown会被一次又一次地触发;当按键被释放时,最终会得到
keyup。因此,有很多keydown却只有一个keyup是很正常的。同时,对于由自动重复触发的事件,
event对象的event.repeat属性被设置为true。
3.5.6 浏览器默认行为
不同的按键,会对应不同的浏览器默认行为,例如:
- 出现在屏幕上的一个字符(最明显的结果)。
- 一个字符被删除(Delete 键)。
- 滚动页面(PageDown 键)。
- 浏览器打开“保存页面”对话框(Ctrl+S)
- ……。
阻止对 keydown 的默认行为可以取消大多数的行为,但基于 OS 的特殊按键除外。
- 例如,在 Windows 中,Alt+F4 会关闭当前浏览器窗口。同时,这无法通过在 JavaScript 中阻止默认行为来阻止它。
备注:过去曾经有一个 keypress 事件,还有事件对象的 keyCode、charCode 和 which 属性。这些目前都不再使用,放在这里是为了完备性。
3.6 滚动事件
scroll 事件:监听对页面或元素的滚动,例如:
- 根据用户在文档中的位置显示/隐藏其他控件或信息。
- 当用户向下滚动到页面末端时加载更多数据。
防止滚动:
在导致滚动的事件上,例如在 pageUp 和 pageDown 的 keydown 事件上,使用 event.preventDefault() 。
4 表单和控件
4.1 表单
4.1.1 navigation - 导航属性
这些导航(navigation)属性用于获取文档 document 中的所有表格和其成员。其并不依赖于标签的结构。
- 所有的表单,无论在文档中的什么位置,都可以通过
document.forms直接获取到。 - 所有的控件元素,无论在表单中有多深,都可以通过
form.elements直接获取到。
document.forms:保存了文档中的所有表单,是一个特殊的集合。
该集合是有序的、被命名的,其具体的文档访问,可以:
- 按名称访问:
document.forms.someElem,访问名称为 someElem 的表单。 - 按下标访问:
document.forms[3],访问文档中第四个表单。
form.elements:保存了 form 表单中的所有元素/成员,是一个特殊的集合。其具体的成员访问,可以“
- 按名称访问:
form.elements.one,访问名称为 one 的控件。- 如果元素的名称有重复的,会返回一个集合,这很常见。
- 缩写名称访问:
form.one,访问名称为 one 的控件。 - 缩写下标访问:
form[1],访问第二个控件。
element.form:保存了该成员/元素所属的表单,即可以通过该属性反向引用表单。
<form name="someOne">
<input name="one" value="1">
<input name="two" value="2">
<input name="two" value="2">
</form>
<script>
let form = document.forms.my // 获取 my表单
let elem = form.elements.one // 获取 one元素
let elems = form.elements.two // 获取的元素是一个集合,保存了两个 two元素
console.log(form === elem.form) // true 可以通过元素反向访问表单
</script>
4.1.2 表单元素
4.1.2.1 input 和 textarea
input.value:获取<input>标签的对应内容值,字符串。
textarea.value:获取<textarea>标签的对应内容值,字符串。
input.checked:获取<checked>标签(复选框)的选中状态,布尔值(true、false)。
4.1.2.2 select 和 option
HTML <select> 标签,是一个提供选项菜单的控件:
<select> 元素有 3 个重要的属性:
select.options:<option>的子元素的集合,即菜单的可选项;select.value:当前所选择的<option>的value值,也就是已选择的<option>;select.selectedIndex:当前所选择的<option>的编号。
它们提供了三种为 <select> 设置 value 的不同方式:
- 找到对应的
<option>元素,并将option.selected设置为true。 - 将
select.value设置为对应的value。 - 将
select.selectedIndex设置为对应<option>的编号。
<select id="select">
<option value="apple">Apple</option>
<option value="pear">Pear</option>
<option value="banana">Banana</option>
</select>
<script>
// 所有这三行做的是同一件事
select.options[2].selected = true;
select.value = 'banana';
select.selectedIndex = 2;
</script>

图片可以看到,默认的 <option> 是 Banana。
快速创建 <option> 元素:
<option>元素就是在<select>,<optgroup>,<datalist>元素中包含的项。
option = new Option(text, value, defaultSelected, selected);
参数:
text:<option>中的文本,value:<option>的value,defaultSelected:如果为true,那么selectedHTML-特性(attribute)就会被创建,selected:如果为true,那么这个<option>就会被选中。- 通常,这后两个值都设置为
true,或者都不设置(与设置为false是一样的)。
- 通常,这后两个值都设置为
<option> 元素具有以下属性:
option.selected:<option>是否被选择。option.index:<option>在其所属的<select>中的编号。option.text:<option>的文本内容(可以被访问者看到)。
4.2 聚焦
focus:为聚焦。当聚焦到一个元素的时候,通常隐含了 “正准备在此处接受数据”,此时常运行代码,用以初始化来接受数据。
blur:为失去焦点。当一个元素失去焦点的时候,通常隐含了 “此处的数据已经输入完成”,此时运行代码检查到该状态,可以把数据保存起来。
获得焦点:
autofocus:一个 HTML 特性(attribute),可以让焦点在网页加载时默认落在一个元素上。- 鼠标:当用户点击某个元素时,该元素将会获得聚焦(focus)。
- 键盘:当用户使用键盘上的
Tab键选中时,该元素将会获得聚焦(focus)。 - JS代码:
elem.focus()和elem.blur()设置和移除元素上的焦点。 - ....
失去焦点:
- 鼠标:用户点击了其它位置。
- 键盘:用户按下
Tab。 - JavaScript:一个
alert会将焦点移至自身,因此会导致元素失去焦点(触发blur事件)。- 当
alert对话框被取消时,焦点又回重新回到原元素上(触发focus事件)。
- 当
- JavaScript:聚焦的元素被从 DOM 中移除,会导致焦点丢失(触发
blur事件)。- 如果稍后它被重新插入到 DOM,焦点也不会回到它身上(不触发
focus事件)。
- 如果稍后它被重新插入到 DOM,焦点也不会回到它身上(不触发
同时,当元素聚焦时,会触发 focus 事件,当元素失去焦点时,会触发 blur 事件。
4.2.1 focus / blur 事件
举例,当需要用户输入邮箱时,可以使用:
blur事件的处理程序,检查用户是否输入正确的电子邮箱,如果不是,就给用户一个提示;focus事件的处理程序,当用户聚焦到输入框(<input>)时,就隐藏用户提示。
<style>
.invalid { border-color: red; }
#error { color: red }
</style>
Your email please: <input type="email" id="input">
<div id="error"></div>
<script>
input.onblur = function() {
if (!input.value.includes('@')) { // 非常简陋的验证,是不是一个邮箱
input.classList.add('invalid'); // input标签添加 无效属性,CSS代码中有,变为红色框。
error.innerHTML = 'Please enter a correct email.' // 提示用户
}
};
input.onfocus = function() {
if (this.classList.contains('invalid')) {
this.classList.remove('invalid'); // 移除 "error" 指示,因为用户想要重新输入一些内容
error.innerHTML = ""; // 删除提示
}
};
</script>
输入正确:

输入错误:

elem.focus() 和 elem.blur() 方法可以设置和移除元素上的焦点。
- 注意,无法通过在
onblur事件处理程序中调用event.preventDefault()来“阻止失去焦点”, 因为元素先失去焦点,然后才触发onblur事件。onblur事件处理程序是在元素失去焦点 之后 运行的。
4.2.2 允许在任何元素上聚焦:tabindex
许多元素浏览器默认是不允许聚焦的,使用 tabindex 则让这些不支持聚焦的元素,变得可以聚焦。
<div>,<span>和<table>,默认是不能被聚焦的。但是可以通过该 HTML 元素特性改变。
该特性的要点是点击 tab 切换聚焦的顺序问题,按照一下优点级依次切换:
tabindex ≥ 1:数值为正数的为第一等级,数字越小,越排在前面;tabindex = 0+ 默认有聚焦的元素(如<input>):为第二等级,按照文档先后顺序决定切换次序。tabindex = -1:只允许以编程的方式聚焦(如elem.focus()),用户鼠标也可以,但tab无法聚焦。
举例:
<ul>
<li tabindex="1">One</li>
<li tabindex="0">Zero</li>
<li tabindex="2">Two</li>
<li tabindex="-1">Minus one</li>
</ul>
<style>
li { cursor: pointer; }
:focus { outline: 1px dashed green; }
</style>
- 顺序就像这样:
1 - 2 - 0。 Minus one不可以被tab聚焦,但是可以被- 通常
<li>不支持聚焦,但tabindex可以使它能聚焦,并且还带有事件以及:focus样式。
focus/blur 委托
focus 和 blur 事件不会向上冒泡。
这里有两个解决方案。
方案一:遗留下来的有趣的特性(feature):
focus/blur不会向上冒泡,但会在捕获阶段向下传播。方案二:使用
focusin和focusout事件 ,与focus/blur事件完全一样,只是它们会冒泡。- 必须使用
elem.addEventListener来分配它们,而不是on<event>。
- 必须使用
4.3 数据更新的相关事件
可以参考文章的例子来记忆:事件:change,input,cut,copy,paste (javascript.info)
数据更改事件:
| 事件 | 描述 | 特点 |
|---|---|---|
change | 值被改变。 | 对于文本输入,当失去焦点时触发。 |
input | 文本输入的每次更改。 | 立即触发,与 change 不同。 |
cut/copy/paste | 剪贴/拷贝/粘贴行为。 | 行为可以被阻止。event.clipboardData 属性可以用于读/写剪贴板。 |
4.3.1 change 事件
当元素更改完成时,将触发 change 事件。
- 对于文本输入框,当其失去焦点时,就会触发
change事件。 - 其它元素:
select,input type=checkbox/radio,在选项更改后立即触发change事件。
4.3.2 input 事件
当用户对输入值进行修改后,就会触发 input 事件。
- 与键盘事件不同,只要值发生改变,
input事件就会触发。使用鼠标粘贴,或者使用语音识别来输入文本也会改变。 - 比如键盘输入:"123",每输入一个字母,就触发一次。一共会触发三次
input事件。 - 如果不更改文本内容,不会出发
change事件,比如按方向键进行光标移动。
4.3.3 cut,copy,paste 事件
这些事件发生于剪切 / 拷贝 / 粘贴一个值的时候。
它们属于 ClipboardEvent 类,并提供了对拷贝/粘贴的数据的访问方法。
可以使用
event.preventDefault()来中止行为,然后什么都不会被复制/粘贴。不仅可以对文本操作,复制 / 粘贴所有内柔都有效果:图片、OS文件管理器中的文件等。
在规范中 有一系列方法,这些方法可用于不同的数据类型,包括文件,对剪贴板(clipboard)进行读/写。
但是请注意,剪贴板是“全局”操作系统级别的。安全起见,大多数浏览器仅在特定的用户行为下,才允许对剪贴板进行读/写,例如在
onclick事件处理程序中。
4.4 表单的提交 submit
submit 事件:在提交表单时被触发。
- 用于在将表单发送到服务器之前对表单进行校验,或者中止提交,并使用 JavaScript 来处理表单。
form.submit() 方法:允许从 JavaScript 启动表单发送。
- 使用此方法动态地创建表单,并将其发送到服务器。
提交表单,也就是触发表单的 submit 事件。主要有两种方式:
- 点击
<input type="submit">或<input type="image">。 - 在
input字段中,(文本输入完毕后)按下Enter键。 - 调用
form.submit()。手动将表单提交到服务器。- 有时该方法被用来手动创建和发送表单。
- 处理程序可以检查数据,如果有错误,就显示出来,并调用
event.preventDefault(),这样表单就不会被发送到服务器了。 - 注:点击
<input type="submit">和<input type="image">。 - 注:在输入框中使用 Enter 发送表单时,会在
<input type="submit">上触发一次click事件。
5 加载文档和其他资源
5.1 页面生命周期
HTML 页面的生命周期包含三个重要事件:
DOMContentLoaded:浏览器已完全加载 HTML,并构建了 DOM 树。load:浏览加载完成了所有外部资源:图片,样式等。beforeunload/unload:当用户正在离开页面时。
每个事件都是有用的,依时间顺序:
DOMContentLoaded事件 :DOM 已经就绪,因此处理程序可以查找 DOM 节点,并初始化接口。load事件 :外部资源已加载完成,样式已被应用,图片大小也已知了。unload事件 :用户几乎已经离开了,但是我们仍然可以启动一些操作,例如发送统计数据。beforeunload事件 : 用户正在离开,可以检查用户是否保存了更改,并询问他是否真的要离开。
5.1.1 DOMContentLoaded 事件
DOMContentLoaded 事件发生在 document 对象上。
必须使用 addEventListener 来捕获它:
document.addEventListener("DOMContentLoaded", ready);
// 不是 "document.onDOMContentLoaded = ..."
5.1.1.1 <script> 脚本的阻塞
当浏览器处理一个 HTML 文档时,会优先把所有 script 标签内的 Js 代码全部运行完毕,然后 DOM 才全部构建完毕,最后触发 DOMContentLoaded 事件。
- 执行流程:浏览器顺次处理 HTML 文档中的代码,同时在构建 DOM。每当遇到一个
<script>标签,就停止构建 DOM,运行处理标签内的 JavaScript 代码。运行完毕后,继续构建 DOM。当 DOM 全部构建完毕(此时所有<script>也都执行完毕)后,才触发DOMContentLoaded事件。 - 原因:为什么会先执行
<script>内的代码,暂停 DOM 构建?- 一种防范措施。脚本(JavaScript 代码)可能想要修改 DOM,甚至对其执行
document.write操作。
- 一种防范措施。脚本(JavaScript 代码)可能想要修改 DOM,甚至对其执行
DOMContentLoaded 事件的阻塞:不论 <script> 标签在 HTML 文档的什么位置(开头 / 结尾),都会优先被执行完毕,才触发 DOMContentLoaded 事件。以下两种情况不会阻塞 DOMContentLoaded 事件:
- 具有
async特性(attribute)的脚本(JavaScript)不会阻塞; - 使用
document.createElement('script')动态生成,并添加到网页的脚本也不会阻塞 。
一个 CSS 样式表的陷阱:
外部样式表不会影响 DOM,因此 DOMContentLoaded 不会等待它们。但是,如果在样式后面有一个脚本,那么该脚本必须等待样式表加载完成:
<link type="text/css" rel="stylesheet" href="style.css">
<script>
// 在样式表加载完成之前,该脚本都不会执行,
alert(getComputedStyle(document.body).marginTop);
</script>
原因是,脚本可能想要获取元素的坐标和其他与样式相关的属性,如上例所示。因此,它必须等待样式加载完成。
当 DOMContentLoaded 等待该脚本时,它现在也在等待该脚本前面的样式。
总结:可以阻塞 DOMContentLoaded 事件的 2 种情况,实质上就是发生了对 DOM 流构建的阻塞:
- HTML 文档流中,有
<script>脚本,需要优先执行所有脚本,最后触发DOMContentLoaded事件。 - HTML 文档流中,CSS样式表后,紧跟一个
<script>脚本,需要优先执行 CSS样式表,然后执行脚本,最近在触发DOMContentLoaded事件。
5.1.1.2 浏览器的自动填充
Firefox,Chrome 和 Opera 都会在 DOMContentLoaded 事件中,自动填充表单。
也就是说,如果页面有一个带有登录名和密码的表单、并且浏览器记住了这些值、同时得到了用户允许,那么在 DOMContentLoaded 事件触发时,浏览器会尝试自动填充它们。
- 有些网页 DOM 流构建需要很长时间,所以会导致
DOMContentLoaded触发会有明显的延迟感,那么自动填充也会等待许久。
5.1.2 load 事件
当整个页面,包括样式、图片和其他资源被加载完成时,会触发 window 对象上的 load 事件。
window.onload设置监听。window.addEventListener('load', (event) => { .... };方法设置监听。
5.1.3 beforeunload 、unload 事件
5.1.3.1 触发的时机
以下情况均会触发这两个事件:
- 在浏览器地址栏输入地址,然后点击跳转;
- 点击页面的链接实现跳转;
- 关闭或刷新当前页面;
- 操作当前页面的
Location对象,修改当前页面地址; - 调用
window.navigate实现跳转; - 调用
window.open或document.open方法在当前页面加载其他页面或重新打开输入流。
5.1.3.2 触发的顺序
- 先
beforeunload; - 后
unload。
5.1.3.3 触发时环境
beforeunload 事件
- 页面所有资源均未释放,且页面可视区域效果没有变化;
- UI 人机交互失效 (
window.open,alert,confirm全部失效); - 最后一个可以阻止
unload过程执行的时机。beforeunload事件的Cancelable属性值为Yes。
unload 事件
- 页面所有资源均未被释放,但是页面可视区域一片空白;
- UI 人机交互失效 (
window.open,alert,confirm全部失效); - 没有任何操作可以阻止
unload过程的执行。(unload事件的Cancelable属性值为No。
5.1.4 unload 事件
当访问者离开页面时,window 对象上的 unload 事件就会被触发。
可以在那里做一些不涉及延迟的操作,例如关闭相关的弹出窗口。
可以收集有关页面使用情况的数据:鼠标点击,滚动,被查看的页面区域等。当用户要离开时,通过
unload事件将数据保存到服务器上。navigator.sendBeacon(url, data)使用该方法,异步(后台)发送数据。即使要跳转另一个页面也不会有延迟:浏览器离开页面,但仍然在执行sendBeacon。使用方法:let analyticsData = { /* 带有收集的数据的对象 */ };
window.addEventListener("unload", function() {
navigator.sendBeacon("/analytics", JSON.stringify(analyticsData));
});请求以 POST 方式发送,数据大小限制在 64kb。
允许发送字符串、表单、其他格式的数据,在 Fetch 一章有详细讲解。但通常它是一个字符串化的对象。
5.1.5 beforeunload 事件
如果访问者触发了页面跳转,或试图关闭窗口,就会触发 beforeunload 事件。
当用户想切换页面,可以在此事件中询问用户是否确定要关闭网页。
window.onbeforeunload = function() {
return false;
};
5.1.6 document.readyState 属性
Document.readyState 属性描述了 document 的加载状态(DOM构建完毕了吗?)。
当该属性值发生变化时,会在 document 对象上触发 readystatechange 事件。
它有 3 个可能值:
loading:文档正在被加载。interactive:文档被全解析,但诸如图像、样式表和框架之类的子资源仍在加载。complete:文档和所有子资源已完成加载。load事件即将被触发。
可以在状态发生改变时,通过触发 readystatechange 事件,查看当前 readyState 属性值:
// 当前状态
console.log(document.readyState);
// 状态改变时打印它
document.addEventListener('readystatechange', () => console.log(document.readyState));
readystatechange 事件是跟踪文档加载状态的另一种机制,现在很少被使用。
为了完整起见,让我们看看完整的事件流。这是一个带有 <iframe>,<img> 和记录事件的处理程序的文档:
<script>
log('initial readyState:' + document.readyState);
document.addEventListener('readystatechange', () =>
log('readyState:' + document.readyState));
document.addEventListener('DOMContentLoaded', () => log('DOMContentLoaded'));
window.onload = () => log('window onload');
</script>
<iframe src="iframe.html" onload="log('iframe onload')"></iframe>
 => log('img onload');
</script>
此示例运行 在 sandbox 中。
典型输出:
- [1] initial readyState:loading
- [2] readyState:interactive
- [2] DOMContentLoaded
- [3] iframe onload
- [4] img onload
- [4] readyState:complete
- [4] window onload // window.onload 在所有 onload 最后运行
方括号中的数字表示发生这种情况的大致时间。标有相同数字的事件几乎是同时发生的(± 几毫秒)。
- 在
DOMContentLoaded之前,document.readyState会立即变成interactive。- 它们俩的意义实际上是相同的。
- 当所有资源(
iframe和img)都加载完成后,document.readyState变成complete。- 这里可以发现,它与
img.onload(img是最后一个资源)和window.onload几乎同时发生。转换到complete状态的意义与window.onload相同。区别在于window.onload始终在所有其他load处理程序之后运行。
- 这里可以发现,它与
5.1.7 总结
页面生命周期事件:
- 当 DOM 准备就绪时,
document上的DOMContentLoaded事件就会被触发。- 在这个阶段,我们可以将 JavaScript 应用于元素。
- 诸如
<script>...</script>或<script src="..."></script>之类的脚本会阻塞。 - 图片和其他资源仍然可以继续被加载。
- 当页面和所有资源都加载完成时,
window上的load事件就会被触发。- 很少使用,因为通常无需等待那么长时间。
- 当用户想要离开页面时,
window上的beforeunload事件就会被触发。- 如果取消这个事件(
return false),浏览器就会询问我们是否真的要离开。
- 如果取消这个事件(
- 当用户最终离开时,
window上的unload事件就会被触发。- 在处理程序中,只能执行不涉及延迟或询问用户的简单操作。正是由于这个限制,很少被使用。
- 通常使用
navigator.sendBeacon来发送网络请求。
5.2 加载脚本 - defer / async / 动态
上文提到过,当浏览器加载 HTML,构建 DOM 的时候,遇到 <script>...</script> 标签,浏览器就需要暂停 DOM 构建,而优先处理脚本中的 JavaScript 代码,
这样会造成两个问题:
- 脚本在运行时,无法访问位于它们下方的 DOM 元素,也无法对这些元素进行操作(设置监听、修改内容等)
- 如果 HTML 文档中间,有一个量很大的脚本,就会发生严重的 “页面阻塞”,脚本在没有下载、解析完之前, DOM 无法构建完毕,用户无法看到文档内容。
解决方案:
- 一个常规性的动作,就是把脚本放置在页面的底部,
</body>的上边,这样它可以顺利的访问所有 DOM 元素,切不会影响 DOM 构建。 - 利用两个
<script>特性(attribute)也可以解决这个问题:defer和async。 - 最后,利用动态方式加载脚本,也是方案之一。
把总结放在开头:
async 和 defer 有一个共同点:加载这样的脚本都不会阻塞页面的渲染。
不同点是:
| 顺序 | DOMContentLoaded | |
|---|---|---|
async | 加载优先顺序。脚本在文档中的顺序不重要 —— 先加载完成的先执行 | 不相关。可能在文档加载完成前加载并执行完毕。如果脚本很小或者来自于缓存,同时文档足够长,就会发生这种情况。 |
defer | 文档顺序(它们在文档中的顺序) | 在文档加载和解析完成之后(如果需要,则会等待),即在 DOMContentLoaded 之前执行。 |
在实际开发中,
defer用于需要查看完整 DOM 的脚本,或需要严格脚本执行顺序的时候。async用于独立脚本,例如计数器或广告,这些脚本的相对执行顺序无关紧要。
注意:
没有脚本的页面应该先给用户一定的提示,例如 “正在加载” 的提示。同时禁用尚不可用的按钮。 以让用户可以清楚地看到,他现在可以在页面上做什么、还有什么是正在准备中的。
5.2.1 defer
defer -- 推迟。
<script defer src="https://xxxxx"> </script>
特性告诉浏览器遇到脚本不会等待,而是继续处理后面的 HTML 结构,构建 DOM。脚本会异步(后台) 下载,等 DOM 构建完成后,脚本才会执行。
具有 defer 特性的脚本:
- 不会阻塞页面。
- DOM 解析完毕后、
DOMContentLoaded事件之前执行。 - 依次执行。多个
defer脚本,保持相对顺序,就像常规脚本一样(按正常的次序,依次执行)。- 如果遇到了两个脚本,按 HTML 中的次序依次是:
large.js和small.js。当浏览器依次扫描到这两个脚本时,把它们丢到异步下载。或许排在后面的small.js先下载完成,但是也会等待large.js下载完毕并且执行后,才会执行。
- 如果遇到了两个脚本,按 HTML 中的次序依次是:
- 仅用于外部脚本。如果
<script>脚本没有src,则会忽略defer特性。
5.2.2 async
async -- 异步。
<script async src="https://xxxxx"> </script>
async 特性与 defer 相同的时,它也能够让脚本不阻塞页面。
不同的是,async 特性意味着脚本是完全独立的,和其他脚本、HTML 文档流是异步关系。
具有 async 特性的脚本:
- 不会阻塞页面。
- 一旦异步下载完毕,立即执行。这意味着:
- 先来先执行。多个
async脚本之间没有执行顺序,先下载完成先执行。 - DOM 构建和脚本异步。一旦 DOM 构建完毕,就会触发
DOMContentLoaded事件,不会等待脚本。DOMContentLoaded可能发生在异步脚本之前(如果异步脚本在页面完成后才加载完成)DOMContentLoaded可能发生在异步脚本之后(如果异步脚本很短,或者是从 HTTP 缓存中加载的)
- 先来先执行。多个
应用:当我们将独立的第三方脚本集成到页面时,采用异步加载方式是非常棒的:计数器,广告等,因为它们不依赖于我们的脚本,我们的脚本也不应该等待它们。
5.2.3 动态加载
使用 JavaScript 动态地创建一个脚本,并将其附加(append)到文档(document)中:
let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // 添加脚本
默认情况下,动态脚本的行为是 “异步” 的,采用 async 方式加载,也就是说:
- 不会等待其他结构,执行到该代码,就会丢到异步去加载。
- 先来先执行。先加载完成的脚本先执行。
可以调整为 defer 方式加载动态脚本,通过显式地设置 script.async=false,此时:
- 脚本将按照在文档中的顺序执行。
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
script.async = false; // 采用 defer 执行
document.body.append(script);
}
loadScript("long.js"); // long.js 先执行,因为代码中设置了 async=false
loadScript("small.js");
5.3 资源加载 - onload / onerror
浏览器允许我们跟踪外部资源的加载 —— 脚本,iframe,图片等。基本上适用于具有 src 特性的所有资源,有两个事件:
onload:成功加载;onerror:出现错误。
5.3.1 onload / onerror
上文提到过,可以利用创建一个 <script> 标签,然后添加到 HTML 结构中,来动态的加载脚本。
但需要注意的是,只有当该脚本成功的下载、加载完成,才可以在其他脚本中使用该脚本创建的函数。
script.onload 事件,会在脚本加载、并执行完成时触发。
script.onerror 事件,会在脚本加载期间出现 error 时触发。
- 比如请求一个不存在的脚本,加载 404 都会发生加载错误。
let script = document.createElement('script');
script.src = "my.js";
document.head.append(script); // 动态加载脚本
script.onload = function(){
console.log("脚本加载成功");
}
script.onerror = function(){
console.log("脚本加载出现错误");
}
onload/onerror事件仅跟踪加载本身,即只监听当前资源是否出现加载错误。window.onerror全局监听,可以监听脚本中,是否出现编程错误。
5.3.2 跨源策略
更多跨源问题,会在 fetch 中讲解,这里主要是为了解释脚本 error 报错追踪的问题。
一条规则:来自一个网站的脚本无法访问其他网站的内容。
- 例如,
https://facebook.com的脚本无法读取位于https://gmail.com的用户邮箱。
更确切地说,一个源(域 / 端口 / 协议)无法获取另一个源(origin)的内容。因此,即使我们有一个子域,或另一个端口,这都是不同的源,彼此无法相互访问。
- 所以,如果网页使用的是来自其他域的脚本,并且该脚本中存在 error,那么网页就无法获取 error 的详细信息。
为什么我们需要 error 的详细信息?
- 有很多服务使用
window.onerror监听全局 error,同时保存 error 信息,并提供访问和分析 error 的接口。这样程序员就可以看到由用户触发的实际中的 error。但是,如果一个脚本来自于另一个源(origin),就无法准确获取更多的 error 的信息。
解决方案:
- CORS: 一个W3C标准,全程跨域资源共享 (Cross-Origin Resource Sharing)。
要允许跨源访问,<script> 标签需要具有 crossorigin 特性(attribute),并且远程服务器必须提供特殊的 header。
这里有三个级别的跨源访问,具体内容参考 fetch 章节。
6 杂项
6.1 MutationObserver
MutationObserver 是 DOM 变动观察器,一个内建对象,它可以随时监听 DOM 元素,并在检测到更改时触发回调。
创建一个带有回调函数的观察器:
let observer = new MutationObserver(callback);然后将其附加到一个 DOM 节点:
elem.observe(node, config);
config是一个具有布尔选项的对象,该布尔选项表示“将对哪些更改做出反应”:childList:监听node的直接子节点的更改;subtree:监听node的所有后代的更改;attributes:监听node的特性(attribute);attributeFilter:特性名称数组,只监听选定的特性;characterData:是否监听node.data(文本内容);attributeOldValue:如果为true,则将特性的旧值和新值都传递给回调,否则只传新值(需要attributes)。characterDataOldValue:如果为true,则将node.data的旧、新值都传递给回调,否则只传新值(需要characterData
- 在发生任何更改后,将执行“回调”:
- 记录的内容,包装为一个 MutationRecord 对象列表传入第一个参数,而观察器自身作为第二个参数。
- MutationRecord 对象具有以下属性:
- type :变动类型,以下类型之一:
"attributes":特性被修改了;"characterData":数据被修改了,用于文本节点;"childList":添加/删除了子元素。
target:更改发生在何处:"attributes"所在的元素,"characterData"所在的文本节点,"childList"变动所在的元素,
addedNodes/removedNodes:添加/删除的节点,previousSibling/nextSibling:添加/删除的节点的上一个/下一个兄弟节点,attributeName/attributeNamespace:被更改的特性的名称/命名空间(用于 XML),oldValue:旧值,仅适用于特性或文本更改,如果设置了相应选项attributeOldValue/characterDataOldValue。
- type :变动类型,以下类型之一:
let observer = new MutationObserver(mutationRecords => {
console.log(mutationRecords); // 发生变化,就会打印。
});
// 观察除了特性之外的所有变动
observer.observe(elem, {
childList: true, // 监听直接子节点
subtree: true, // 监听其更低的后代节点
characterDataOldValue: true // 将旧的数据传递给回调
});
其他方法:
observer.disconnect() :停止观察。
observer.takeRecords() :获取尚未处理的变动记录列表,表中记录的是已经发生,但回调暂未处理的变动。
- 当我们停止观察时,观察器可能尚未处理某些更改,使用该方法可以获取这些信息。
// 如果你关心可能未处理的近期的变动
// 那么,应该在 disconnect 前调用获取未处理的变动列表
let mutationRecords = observer.takeRecords();
// 停止跟踪变动
observer.disconnect();
...
6.1.1 应用方法:
.1 用于集成
如果要在网页中添加一个第三方脚本,该脚本可能会执行一些负面操作,例如显示广告
<div class="ads">ads</div>。我们可以使用MutationObserver,监测到我们不需要的元素何时出现在我们的 DOM 中,并将其删除。还有一些其他情况,例如第三方脚本会将某些内容添加到我们的文档中,并且我们希望检测出这种情况何时发生,以调整页面,动态调整某些内容的大小等。
MutationObserver使我们能够实现这种需求。
.2 用于架构
假设我们正在建立一个有关编程的网站。自然地,文章中可能包含一些源代码段,我们通常会对源代码段进行一定的美化工作。如果文章是我们直接加载好的,那很简单,直接利用 JavaScript 脚本对源代码内容进行修改即可。
但有可能是动态加载的文章,此时可以用 MutationObserver 监测何时在页面中插入了新的代码段,然后运行相应的美化代码。
6.2 Selection / Range
这里将介绍文档中的选择,以及在表单字段(如 <input>)中的选择。
JavaScript 可以获取现有选择,也可以 选择 / 取消全部 或 部分选择,从文档中删除所选部分,将其包装到一个标签(tag)中,等等操作。
总体来说,根据要选择的内容,可划分为以下两种选择方式:
- 对于文档:
Selection和Range对象。 - 对于
input,textarea:其他方法和属性。
6.2.1 范围 Range
选择的基本概念是 Range:本质上是一对“边界点”:范围起点和范围终点。
let range = new Range();
利用相对于起点(父节点)的相对偏移(offset)值,确定每个点的位置:
range.setStart(parentNode, offset); // 包括 offset
range.setEnd(parentNode, offset); // 不包括 offset
- 我们不必在
setStart和setEnd中使用相同的 node 节点。一个范围可能跨越许多不相关的节点。唯一要注意的是终点要在起点之后。
举例1:
<p id="p">Example: <i>italic</i> and <b>bold</b></p>
- 选择
"Example: <i>italic</i>"。它是<p>的前两个子节点(文本节点也算在内):
let range = new Range();
range.setStart(p, 0); // 设置起点,从下标 0 开始
range.setEnd(p, 2); // 设置终点,到下标 2 截止,不包括 2.
alert(range); // Example: italic ,range的 toString形式直接输出文本内容。
document.getSelection().addRange(range);
举例2,范围跨越不同的 node 节点:
<p id="p">Example: <i>italic</i> and <b>bold</b></p>
<script>
let range = new Range();
range.setStart(p.firstChild, 2);
range.setEnd(p.querySelector('b').firstChild, 3);
alert(range); // ample: italic and bol
window.getSelection().addRange(range); // 将范围应用于文档选择
</script>
6.2.1.1 range 的属性

startContainer,startOffset:起始节点和偏移量,- 在上例中:分别是
<p>中的第一个文本节点和2。
- 在上例中:分别是
endContainer,endOffset:结束节点和偏移量,- 在上例中:分别是
<b>中的第一个文本节点和3。
- 在上例中:分别是
collapsed:布尔值。如果范围在同一点上开始和结束( range 内没有内容)则为true,- 在上例中:
false
- 在上例中:
commonAncestorContainer:在范围内的所有节点中最近的共同祖先节点。- 在上例中:
<p>
- 在上例中:
6.2.1.2 range 的方法
设置范围的起点:
setStart(node, offset)将起点设置在:node中的位置offsetsetStartBefore(node)将起点设置在:node前面setStartAfter(node)将起点设置在:node后面
设置范围的终点:
setEnd(node, offset)将终点设置为:node中的位置offsetsetEndBefore(node)将终点设置为:node前面setEndAfter(node)将终点设置为:node后面
node 既可以是文本节点,也可以是元素节点:
- 对于文本节点,
offset偏移的是字符数, - 对于元素节点,
offset偏移是子节点数。
其他:
selectNode(node):设置一个选择整个node的 rangeselectNodeContents(node):设置一个选择整个node内容的 range。collapse(toStart):折叠 range。如果toStart=true则设置 end=start,否则设置 start=end。cloneRange()创建一个具有相同起点/终点的新 range。
如要操纵范围内的内容:
deleteContents():从文档中删除范围内容。extractContents():从文档中删除范围内容,并将删除的内容作为 DocumentFragment 返回。cloneContents():复制范围内容,并将复制的内容作为 DocumentFragment 返回。insertNode(node):在范围的起始处将node插入文档。surroundContents(node):使用node将所选范围内容包裹起来。- 要使此操作有效,则该范围必须包含其中所有元素的开始和结束标签:不能像
<i>abc这样的部分范围。
- 要使此操作有效,则该范围必须包含其中所有元素的开始和结束标签:不能像
6.2.2 选择 selection
Range 是用于管理选择范围的对象。文档选择就是由 Selection 对象表示的,
window.getSelection()或document.getSelection()来获取对象中的文档选择。
用户有许多种选择内容的方式。可能是用鼠标、热键、手机上的点击+拖动等等。
一个 selection 可以包括零个或多个 range 。实际上,只有 Firefox 允许用户使用 Ctrl+click (Mac 上用 Cmd+click) 在文档中选择多个范围。其他浏览器最多支持 1 个范围。
- 某些
Selection方法暗示可能有多个范围,但是在除 Firefox 之外的所有浏览器中,范围最多是 1。
6.2.2.1 selection 的属性
与范围相似,选择的起点称为“锚点(anchor)”,终点称为“焦点(focus)”。
主要的选择属性有:
anchorNode:起始节点,anchorOffset:相对于起始节点anchorNode的偏移量,focusNode:结束节点,focusOffset:相对于结束节点focusNode的偏移量,isCollapsed:是否为空。如果未选择任何内容(空范围)或不存在,则为true。rangeCount:保存该 selection 对象中的范围数。除 Firefox 外,其他浏览器最多为1。
在文档中,selection的终点可能在起点之前,而 Range 的起点必须在开头。
例如鼠标,允许从两个方向创建相同的选择:“从左到右”和“从右到左”。
“===>” forward。选择的起点(anchor)在终点(focus)之前,则称此选择具有 “forward” 方向。
“===>” backward。相反,终点在前面,在成为 backward 方向。
上图为 forward 方向,下图为 backward 方向。
6.3.2.2 selection 的事件
可以追踪发生 selection 的事件:
elem.onselectstart:当用户从elem上开始选择时,触发该事件。- 例如,用户在 elem 元素上,按下鼠标键并开始移动鼠标。
- 阻止默认行为会使选择无法开始。
document.onselectionchange:当选择的区域发生变动时(重新选择、刚开始选择),触发该事件。- 注:此处理程序只能在
document上设置。 - 事实上,第二个事件的第一次触发,就相当于第一个事件被触发。
- 注:此处理程序只能在
6.3.2.3 selection 的方法
添加/移除范围的选择方法:
getRangeAt(i):获取从0开始到第 i 个的全部 range 。- 在除 Firefox 之外的所有浏览器中,仅使用
0。
- 在除 Firefox 之外的所有浏览器中,仅使用
addRange(range):将range添加到 selection 中。- 如果选择已有关联的范围,则除 Firefox 外的所有浏览器都将忽略该调用。
removeRange(range):从 selection 中删除range。removeAllRanges():删除所有 range。empty():removeAllRanges的别名。
直接选择范围,而无需使用 Range:
collapse(node, offset):用一个新的范围替换选定的范围,从node处开始,到偏移offset处结束。setPosition(node, offset):collapse的别名。collapseToStart():折叠(替换为空范围)到选择起点,collapseToEnd():折叠到选择终点,extend(node, offset):将选择的焦点(focus)移到给定的node,位置偏移oofset,setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset):用给定的起点anchorNode/anchorOffset和终点focusNode/focusOffset来替换选择范围。选中它们之间的所有内容。selectAllChildren(node):选择node的所有子节点。deleteFromDocument():从文档中删除所选择的内容。containsNode(node, allowPartialContainment = false):检查选择中是否包含node(特别是如果第二个参数是true的话)
6.3.3 表单控件中的选择
诸如 input 和 textarea 等表单元素提供了 专用的选择 API,没有 Selection 或 Range 对象。
由于输入值是纯文本而不是 HTML,因此也不需要此类对象,直接利用属性和方法即可:
属性:
input.selectionStart:选择的起始位置(可修改),input.selectionEnd:选择的结束位置(可修改),input.selectionDirection:选择方向:“forward”,“backward” 或 “none”。- 例如使用鼠标双击进行的选择时,要朝哪个方向。
事件:
input.onselect:当发生选择时,触发该事件。
方法:
input.select():选择文本控件中的所有内容,input.setSelectionRange(start, end, [direction]):在给定方向上(可选),从start一直选择到end。input.setRangeText(replacement, [start], [end], [selectionMode]):用新文本替换范围中的文本。可选参数
start和end,如果提供的话,则设置范围的起点和终点,否则使用用户的选择。最后一个参数
selectionMode决定替换文本后如何设置选择。可能的值为:"select":将选择新插入的文本。"start":选择范围将在插入的文本之前折叠(光标将在其之前)。"end":选择范围将在插入的文本之后折叠(光标将紧随其后)。"preserve":尝试保留选择。这是默认值。
有许多实例,在 示例:跟踪选择 。
6.3.4 使不可选择
要使某些内容不可选,有三种方式:
- 使用 CSS 属性阻止元素的可被选择:
user-select: none。- 这样不允许选择从
elem开始。但用户可以在其他地方开始选择,并可以把elem包含在内。 - 然后
elem将成为document.getSelection()的一部分,因此选择实际发生了,但是在复制粘贴中,其内容通常会被忽略。
- 这样不允许选择从
- 阻止
onselectstart或mousedown事件中的默认行为。elem.onselectstart = ()=>false和上面一样,用户也可通过从其他元素开始选择,而扩展到这里。
- 使用
document.getSelection().empty()来在选择发生后清除选择。- 很少使用这种方法,因为这会在选择项消失时导致不必要的闪烁。
6.3 事件循环:微任务、宏任务
宏任务:
- 引擎执行任务时(完成一次整体循环),永远不会进行渲染(render)。即使任务事件非常长,也只在完成后才绘制对 DOM 的更改。
- 如果一项任务执行花费的时间过长,浏览器将无法执行其他任务,例如处理用户事件。 因此,在一定时间后,浏览器会抛出一个如“页面未响应”之类的警报,建议任务。
微任务:
- 微任务仅来自于我们的代码。它们通常是由 promise 创建的:
对
.then/catch/finally处理程序的执行会成为微任务。微任务也被用于await的“幕后”,因为它是 promise 处理的另一种形式。
还有一个特殊的函数 queueMicrotask(func),它对 func 进行排队,以在微任务队列中执行。
- 宏任务 --> 微任务 --> 渲染 --> 宏任务 ...。它确保了在同一个宏任务后的多个微任务,应用程序环境基本相同(没有鼠标坐标更改,没有新的网络数据等)。
事件循环算法(这是 规范 的简化版本):
- 任务从 宏任务 队列(例如 “script”)中出队(dequeue)并执行最早的任务。
- 执行所有 微任务 ,依次出队后,执行。
- 执行 渲染,如果有的话。
- 如果宏任务队列为空,则休眠直到出现宏任务。
- 否则,转到步骤 1。
添加一个新的 宏任务:
- 使用零延迟的
setTimeout(f)。- 用于将繁重的计算任务拆分成多个部分,以使浏览器能够对用户事件及时作出反应、或在任务的各部分之间显示任务进度。
- 也用于在事件处理程序中,将一个行为(action)添加在事件被完全处理(冒泡完成)后。
添加一个新的 微任务:
- promise 的相关方法
.then/catch/finally,以及衍生方法generator/async/await。 - 使用
queueMicrotask(f)。- 微任务之间,没有 UI 或网络事件的处理:它们一个接一个地立即执行。
- 所以常用
queueMicrotask来在保持 环境状态一致 的情况下,异步 地执行一个函数。