5. dom
5. dom
Node接口,这是所有dom节点继承的接口,它允许我们使用相似的方式对待这些不同类型的dom对象- 所有的节点类型都继承了这个接口
- 此接口有一个父接口
EventTarget,可以接收事件、并且可以创建侦听器,具体见事件部分
5.1. document
- 在浏览器全局中有一个
Document接口,接口表示任何在浏览器中载入的网页,并作为网页内容的入口- 描述了任何类型的文档的通用属性与方法,可以操作元素节点
- 在浏览器中有一个
document对象是它的实例,可以访问整个文档
document对象有一些属性可以获取到网站信息document.title: 获取和设置当前网页的标题document.cookie: 获取和添加当前网页的cookiedocument.URL: 获取当前网页的URLdocument.location: 获取当前网页的location对象,同时也可以直接赋值字符串,将浏览器跳转到指定URL(实际上是修改location对象的href属性)document.readyState: 获取当前网页的状态loading: 正在加载interactive: 已经加载完成,但是DOM树还没有构建完成complete: DOM树构建完成
document.visibilityState: 获取当前网页的可见状态,一般情况下可以通过事件监听变化hidden: 当前网页不可见visible: 当前网页可见
5.1.1. 获取元素节点
document对象可以通过body、head属性直接访问到body、head节点,也可以通过children访问到根html节点除此之外,
document对象还定义了一些方法,用于获取满足条件的子元素节点getElementById:通过id获取元素节点,返回第一个匹配的元素节点,如果没有匹配的元素节点,则返回nullgetElementsByTagName:通过标签名获取元素节点,返回一个元素节点动态集合HTMLCollection(内容实时更新)getElementsByClassName:通过类名获取元素节点,返回一个元素节点动态集合HTMLCollectiongetElementsByName:通过name属性获取元素节点,返回一个元素节点静态集合NodeList(内容不会变化)querySelector:通过css选择器获取元素节点,返回第一个匹配的元素节点,如果没有匹配的元素节点,则返回nullquerySelectorAll:通过css选择器获取元素节点,返回一个元素节点静态集合NodeList
const byId = document.getElementById('fruit-list'); console.log('getElementById:', byId); // 返回值:<ul id="fruit-list" ...>(Element) const byTag = document.getElementsByTagName('li'); console.log('getElementsByTagName:', byTag, '长度:', byTag.length); // 返回值:HTMLCollection(5),包含5个<li>,动态集合 const byClass = document.getElementsByClassName('item'); console.log('getElementsByClassName:', byClass, '长度:', byClass.length); // 返回值:HTMLCollection(5),包含class="item"的5个<li>,动态集合 const byName = document.getElementsByName('myList'); console.log('getElementsByName:', byName, '长度:', byName.length); // 返回值:NodeList(1),包含<ul name="myList">,静态集合(仅document可调用) const firstSpecial = document.querySelector('.special'); console.log('querySelector:', firstSpecial); // 返回值:第一个class="special"的<li>元素(Element) const allItems = document.querySelectorAll('.item'); console.log('querySelectorAll:', allItems, '长度:', allItems.length); // 返回值:NodeList(5),包含所有class="item"的<li>元素,静态集合其中除了
getElementById、getElementsByName方法外,其他查找方法不依赖全局唯一标识,可以在节点上使用,仅查询子树上的节点
- 通过节点之间的关系查找
- 由于
HTML换行和空格会产生空白文本节点,为了方便获取元素节点,提供了两类获取节点的方法,可以根据是否需要获取文本节点来选择获取方法 - 对文本节点和元素节点可以通过
NodeType属性判断,文本的NodeType为3,元素的为1,当然也可以使用Node.TEXT_NODE等常量判断 - 空白文本节点也就是文本节点中没有内容,
node.textContent.trim() === ''
- 由于
| 属性 / 方法 | 说明 |
|---|---|
element.parentNode | 返回父节点(可能是元素或document) |
element.parentElement | 返回父元素(排除document、文档片段等非元素节点) |
element.children | 返回所有子元素(不含文本节点) |
element.childNodes | 返回所有子节点(含文本、注释) |
element.firstChild / element.lastChild | 返回第一个/最后一个子节点(含文本节点) |
element.firstElementChild / element.lastElementChild | 返回第一个/最后一个子元素节点 |
element.nextSibling / element.previousSibling | 返回相邻的兄弟节点(含文本节点) |
element.nextElementSibling / element.previousElementSibling | 返回相邻的兄弟元素 |
element.contains(node) | 判断当前节点是否包含指定节点node |
element.hasChildNodes() | 检查是否存在子节点 |
- 其他刻获取元素的属性和方法
document.documentElement:获取文档的根元素html(同时也是唯一子元素节点firstElementChildren)document.body:获取文档的 body 元素document.head:获取文档的 head 元素document.script:获取包含所有的 script 元素的列表document.links:获取包含所有的 a 元素和 area 元素的列表document.images:获取包含所有的 img 元素的列表document.forms:获取包含所有的 form 元素的列表document.styleSheets:获取包含所有的 link 元素的列表document.scrollElement:获取滚动文档,应该是documentElement(根元素)document.fullscreenElement:获取当前全屏元素,如果没有全屏元素,则返回 nulldocument.activeElement:获取当前获得焦点的元素document.elementFromPoint(x,y):返回位于浏览器视口指定坐标处的元素
5.1.2. 创建节点
- 通过
js动态创建与定义的节点对象的方法有:document.createElement(tagName):创建一个元素节点,tagName表示节点标签名document.createTextNode(text):创建一个文本节点,用于添加文本document.createComment(text):创建一个注释节点,一般很少使用document.createDocumentFragment():创建一个文档片段,用于存放临时节点,之后批量插入到DOM树document.importNode(node, deep):获取一个来自其他文档的节点副本,一般用于从其他文档比如iframe中复制节点,deep表示是否深度导入element.cloneNode(deep):克隆一个节点,deep表示是否深度克隆
影子根节点
ShadowRoot:元素内部的一个独立DOM树,用来实现 封装的DOM子树,可以隔离样式和DOM,避免影响页面其他部分创建节点
- 选择宿主节点
- 创建影子根节点,设置内部元素的外部可见性
mode,外部需要通过宿主元素的shadowRoot访问,如果mode为open,则外部可以访问到shadowRoot,否则外部不可直接访问 - 为影子根节点添加内容
// 选择宿主节点 const element = document.querySelector('#my-element'); // 创建影子根节点 const shadowRoot = element.attachShadow({ mode: 'open' }); // 为影子根节点添加内容 shadowRoot.innerHTML = `<p>Hello World!</p>` // 也可以使用之后的节点插入方法 shadowRoot.append(document.createElement('p')); // 外部访问 console.log(shadowRoot.querySelector('p').textContent); console.log(element.shadowRoot.querySelector('p').textContent);
- 自定义元素:可以对
HTMLElement进行扩展,创建自定义元素类 - 自定义元素类继承
HTMLElement类,定义元素行为- 需要创建一个名为
observedAttributes的静态属性,包含元素需要变更通知的所有属性名称的数组 - 构造函数重写,并使用
super()初始化HTMLElement类构造 - 完善生命周期钩子函数
connectedCallback():元素插入到DOM时调用disconnectedCallback():元素从DOM中移除时调用adoptedCallback():元素被移动到新的document时调用attributeChangedCallback(name, oldValue, newValue):observedAttributes中的属性值变更时调用
- 需要创建一个名为
- 使用
customElements.define(tagName, class, options)tagName:自定义元素标签名,如my-elementclass:自定义元素类options:定义元素行为,目前只包含extends属性,用于继承其他现有元素,现有元素可以设置is属性,属性值对应自定义元素标签名,表明应该按照什么元素标签来显示(safari浏览器不支持is)
- 在定义之后可以使用
customElements.upgrade(element)将自定义节点绑定样式,之后element instanceof MyElement才为true
// <my-element name="张三" age="18"></my-element>
class MyElement extends HTMLElement {
static get observedAttributes() {
return ['name', 'age'];
}
constructor() {
super();
// 考虑使用影子节点,避免样式冲突
let shadowRoot = this.attachShadow({ mode: 'open' });
const pNode = document.createElement('p');
pNode.textContent = `name: ${this.getAttribute('name')} age: ${this.getAttribute('age')}`;
this.shadowRoot.append(pNode);
this.pNode = pNode;
}
attributeChangedCallback(name, oldValue, newValue) {
this.pNode.textContent = `name: ${this.getAttribute('name')} age: ${this.getAttribute('age')}`;
}
}
customElements.define('my-element', MyElement);内部元素的处理
一个挂载了影子节点的元素中可能有子元素标签,这些子元素将作为影子节点中的<slot/>的内容,这和许多前端框架的插槽功能类似
- 默认插槽:仅使用
<slot/>定义的插槽,所有内容默认放在默认插槽中 - 具名插槽:支持对插槽命名
<slot name="xxx"/>,并在子标签使用slot="xxx"属性表明放入哪个插槽中 - 内容回退:如果没有提供对应插槽的内容,插槽标签中的内容将显示出来
- 内容更新:插槽的内容是实时绑定的,来自元素的子元素标签,只需修改子标签,内容就会更新
- 变化监听:插槽的内容发生变化时,会在
slot元素上触发slotchange事件
5.1.3. 节点插入
将节点插入在指定位置
如果节点来自已在树上的节点引用,也就是获取到的节点,那么插入将变为移动,而不是复制
preappend和append方法插入的内容将保持插入时的顺序,相当于在开头或结尾按参数从左往右添加一些节点before和after和preappend和append方法插入的规则一致replaceWith方法效果相当于先在前或后插入再移除当前节点,不过效率更好,此方法在元素未被挂载到父节点时无效被移除的节点依旧可以通过引用访问,只是不在
DOM树上兼容性
IE:最早仅有appendChild、insertBefore、replaceChild和removeChild方法,也仅有这四个方法有返回值(被插入或删除的节点),这四个方法仅支持一次插入或删除单个节点nodesOrText的参数可以是节点或字符串,字符串会自动转换为文本节点对象有大量需要插入的节点时考虑使用文档片段
DocumentFragmentconst frag = document.createDocumentFragment(); frag.append(child1, child2); parent.append(frag); // child1, child2 被插入 parent,frag 变空
方法 调用者 说明 parent.appendChild(node)父节点 将节点添加为最后一个子节点 parent.insertBefore(newNode,referenceNode)父节点 在指定子节点前插入新节点 parent.replaceChild(newNode,oldNode)父节点 用新节点替换旧节点 parent.removeChild(node)父节点 删除子节点(返回被删除的节点) element.append(...nodesOrText)父节点 可添加多个节点或文本,位置在末尾 element.prepend(...nodesOrText)父节点 可添加多个节点或文本,位置在开头 element.before(...nodesOrText)一般节点 在当前节点之前插入内容 element.after(...nodesOrText)一般节点 在当前节点之后插入内容 element.replaceWith(...nodesOrText)一般节点 用指定内容替换当前元素节点 element.remove()一般节点 直接从文档中移除自身和子树
5.1.4. 元素节点的方法属性
- 元素节点的通用方法属性都是从
HTMLElement继承的,这些属性方法可以在所有的元素节点中使用
基本属性(
Element信息)attributes:属性集合(NamedNodeMap),类数组类型,支持遍历属性,同时支持使用属性名称访问,访问结果是一个属性对象,属性值是属性对象中的value属性,一般直接使用以下属性方法处理属性getAttribute(name):获取属性值字符串setAttribute(name, value):设置属性值hasAttribute(name):检查属性是否存在removeAttribute(name):删除属性toggleAttribute(name, force?):切换布尔属性,第二个参数用于强制设置为true或falsecreateAttribute(name):创建一个属性类型,赋值value后,可以使用setAttribute(attribute)方法直接设置属性dataset:DOMStringMap,访问data-*属性,比如<div id="el" data-user-id="123"></div>可以通过dataset.userId访问到
id:string类型,元素的idtagName:string类型,标签名(大写)nodeName:string类型,节点名,等于tagNamenodeType:number类型,节点类型dir:string类型,元素方向,比如ltr、rtl,autolang:string类型,元素语言,比如en、zh,zh-CNtitle:string类型,元素标题,比如鼠标悬停时显示的提示信息tabIndex:number类型,元素tab键索引顺序(从低到高),0表示元素不能被 tab 访问draggable:boolean类型,元素是否可拖拽,默认为falsehidden:boolean类型,元素是否隐藏,默认为falsetranslate:boolean类型,元素是否可翻译,默认为true- 元素的内部内容:用于返回或设置元素内部内容,
innerHTML被替换将重新生成其中的节点,innerText和contentText被替换将移除其中的节点,改为文本,其中innerText会将\n转换成br标签,而contentText不会,将当成空白文本(作为空格)innerHTML:string类型,元素内部内容,和文件中保持一致,将被按照html解析规则读取成节点结构,不会执行script中的代码,但依旧有可能被XSS注入攻击,比如<img src="x" onerror="alert(1)">中的onerror标签事件内联属性是会执行的innerText:string类型,元素纯内部文本内容,也就是用户看到的文字,其中有因为css样式产生的换行行为textContent:string类型,元素纯内部文本内容,也就是文件中标签除去内部标签以后的剩余文本,换行符也被保留
innerHTML 风险
- 在为了安全或速度考虑时,可能将部分
html结构通过请求获取,但这样可能造成XSS等风险,以下方法可以缓解风险- 使用
Content-Security-Policy也就是CSP控制资源加载地址,不受信的远程地址资源将不被加载,加载白名单- 在响应头中添加
Content-Security-Policy - 在前端
meta标签中添加Content-Security-Policy,禁止内联脚本和不安全功能
- 在响应头中添加
- 后端对需要发送的结构进行清洗,确保服务器发送的内容中不包含脚本和不可信的
url - 前端再清洗一遍,之后可以考虑保存起来
- 对其他来源的内容使用
iframe sandbox加载,<iframe sandbox srcdoc="<p>内容</p>"></iframe>sandbox属性需要填入对应的控制,内部才能使用相应的功能,加载脚本allow-scripts、开放外部源(将用于获取cookie和父页面信息)allow-same-origin、开关新页面或重定向allow-top-navigation
- 使用
http only的cookie等减小恶意脚本带来的损失
- 使用
样式与布局
style:内联样式对象(CSSStyleDeclaration),可以直接挂载属性,属性名会从驼峰转为-连接命名,对应css样式getPropertyValue(name):返回对应的属性值setProperty(name, value, important?):设置属性值,第三个参数表示是否为!importantremoveProperty(name):删除属性
cssText:内联样式字符串,可以直接赋值,和标签的style写法一致,相当于直接操作style属性字符串className:string类型,class字符串classList:管理class字符串列表,间接控制样式,可以使用方法add/remove/toggle/contains添加、删除、切换和判断是否存在某个classoffsetParent:最近的非static定位祖先元素,可能是绝对定位元素的包含块,也可能是以下情况的元素- 使用不同
zoom的祖先元素 - 当是
static元素时,tr/td/table这些祖先元素也可能成为offsetParent - 如果这些情况的祖先元素都没有,则
offsetParent为body display:none或元素是body/html时为nullfirefox下fixed元素定位父元素为body,其他浏览器值为null
- 使用不同
offsetLeft/Top/Width/Height:元素的边框盒子占用空间尺寸和相对于offsetParent的内边距盒子左上角的位置(整数)clientWidth/Height:获取内边距盒子的尺寸(整数),不包含滚动条和边框document.documentElement的clientWidth/Height为视口尺寸,等同于window.innerWidth/Height减去浏览器的滚动条宽度。document.body的clientWidth/Height为页面实际尺寸
scrollWidth/Height:元素内边距盒子的完整大小(整数),包含overflow:hidden;隐藏区域- 如果里面的元素是
absolute元素,负的left和top属性将使元素向左上角移动,使一部分区域不可能被看到(被浏览器忽略),此时scrollWidth和scrollHeight将仅包含可访问区域的大小
- 如果里面的元素是
getBoundingClientRect():元素相对于视口渲染的位置和尺寸(按边框盒子计算的浮点数),会受到缩放的影响,返回一个DOMRect对象,包含以下属性left:元素左边框到视口左边框的距离top:元素上边框到视口上边框的距离right:元素右边框到视口右边框的距离bottom:元素下边框到视口下边框的距离width:元素宽度height:元素高度x:元素左边框到视口左边框的距离y:元素上边框到视口上边框的距离
getClientRects():返回元素的渲染盒DOMRect列表,因为换行和伪元素的存在,渲染盒可能不止一个window.getComputedStyle(element):获取元素样式对象(CSSStyleDeclaration),包含所有样式属性,可以获取到width和height(效果受box-sizing影响)scrollLeft/Top:滚动条位置,初始位置是0,并且值随着向右和向下而增大,增大幅度和内容中的点移动的距离相同。但初始位置会受到文本布局方向的影响,比如direction、write-mode、flex-direction,也就是说展示区域初始会在右上角、左右下角,这时会出现负值scrollIntoView():滚动元素到可见区域
布局属性如
offset/client/scroll/getBoundingClientRect/getComputedStyle的访问会触发立即回流尺寸属性的区别 绝对定位元素定位指南
绝对定位元素通常通过
left、top确定位置,因此定位主要是对left、top的计算- 绝对定位元素是相对包含块左上角定位自身外边距区域的位置,也就是会受到自身的
margin影响 - 需要使用
js动态计算的场景通常和某个动态的clientX和clientY相关 - 计算的重点在于计算这个
clientX和clientY与元素的left、top的相对关系,确保以下等式成立
- 绝对定位元素是相对包含块左上角定位自身外边距区域的位置,也就是会受到自身的
offsetParent可以获取到元素的定位父元素,但最好为父元素添加position: relative,因为offsetParent获取到的元素可能不是定位元素,而是其他的已定位元素获取元素
left、top主要是使用element.offsetLeft、element.offsetTop,需要注意的是offset包含了元素自身的margin和border,需要减去这些宽度才是元素的left、top当然也可以考虑使用
getBoundingClientRect获取元素和父relative边框盒子的视口left、top,获取到的边距包含了margin和相对定位元素的padding,需要减去这个宽度如果是从一般元素转换为
absolute元素时,在有外边距折叠的情况下,margin-top会使元素出现初始的偏移,最方便的解决办法是从一开始就不出现边距折叠,或者还原折叠(需要每个相关元素都有上边距折叠,统一相对偏移额外减去margin-top)
let [resultLeft, resultTop] = [0, 0] const elementStyle = window.getComputedStyle(event.target) // 方法1:基于 offsetLeft 和 offsetTop // 新位置 = 当前鼠标位置 + 初始位置 - 初始鼠标位置 // 应该设置的位置偏移 resultLeft = event.target.offsetLeft - event.clientX - parseFloat(elementStyle.marginLeft) - parseFloat(elementStyle.borderLeft) resultTop = event.target.offsetTop - event.clientY - parseFloat(elementStyle.marginTop) - parseFloat(elementStyle.borderTop) // 方法2:基于 getBoundingClientRect const { left:parentLeft, top:parentTop } = event.target.parentElement.getBoundingClientRect(); const { left, top } = event.target.getBoundingClientRect(); resultLeft1 = event.clientX - left + parentLeft + parseFloat(elementStyle.marginLeft); resultTop1 = event.clientY - top + parentTop + parseFloat(elementStyle.marginTop); // 之后在 mousemove 中设置位置 element.style.left = `${event.clientX + resultLeft}px` element.style.top = `${event.clientY + resultTop}px`其他实用方法
matches(selector): 判断元素是否匹配某个CSS选择器closest(selector): 查找最近的匹配祖先元素(包含自身)contains(node): 检查元素是否包含指定节点focus()/blur(): 聚焦或失去焦点,需要是表单元素或具有tabindex属性,获取到焦点的元素会显示在可视区域中requestFullscreen(): 让元素全屏显示,返回值是Promise对象- 要求在用户点击、按键等事件内才能调用,否则会报错
Pesmission check failed。但经过测试,点击元素后,立刻使用刷新按钮居然会使全局的全屏执行生效 - 退出全屏使用
document.exitFullscreen() document.fullscreenElement属性会返回当前全屏元素,如果没有元素全屏,则返回null
- 要求在用户点击、按键等事件内才能调用,否则会报错
- 图片放大镜中包含了位置转换问题,遇到这种问题需要使用
getBoundingClientRect()获取元素的位置,然后根据缩放倍数进行转换
<div id="img-container">
<div id="hover-block"></div>
<img id="origin-img" src="" alt=""/>
<div id="scale-container">
<img id="scale-pic" src=""/>
</div>
</div>const imgContainer = document.getElementById('img-container')
const img = document.getElementById('origin-img')
const scaleContainer = document.getElementById('scale-container')
const scalePic = document.getElementById('scale-pic')
const hoverBlock = document.getElementById('hover-block')
function svgToBase64(svg) {
const encoded = new TextEncoder().encode(svg); // 转为 UTF-8 bytes
let binary = '';
for (let byte of encoded) {
binary += String.fromCharCode(byte);
}
return btoa(binary);
}
window.onload = () => {
// 定义一个 200×200 SVG,有明显的彩色块
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<defs>
<pattern id="grid" width="50" height="50" patternUnits="userSpaceOnUse">
<rect width="50" height="50" fill="#eee"/>
<rect width="25" height="25" fill="#ff8a00"/>
<rect x="25" y="25" width="25" height="25" fill="#e52e71"/>
</pattern>
</defs>
<rect width="200" height="200" fill="url(#grid)" />
</svg>
`.trim();
// 转换为 Base64 数据 URL
const svgUrl = "data:image/svg+xml;base64," + svgToBase64(svg);
// 设置图片 src
img.src = svgUrl;
scalePic.src = svgUrl;
};
imgContainer.addEventListener('mouseenter', () => {
scaleContainer.style.display = 'block'
hoverBlock.style.display = 'block'
})
imgContainer.addEventListener('mousemove', (e) => {
if (scaleContainer.contains(e.target)) {
scaleContainer.style.display = 'none'
hoverBlock.style.display = 'none'
return
}
const scaleViewRect = scaleContainer.getBoundingClientRect()
const scalePicRect = scalePic.getBoundingClientRect()
const imgRect = imgContainer.getBoundingClientRect()
const scale = scalePicRect.width / imgRect.width
const minLeft = scaleViewRect.width - scalePicRect.width
const minTop = scaleViewRect.height - scalePicRect.height
// 转换坐标,获取到原图放大中心坐标 (clientX, clientY)
// 此坐标减去 放大区域大小/scale/2 对应放大区域左上角应该显示位置
// 再乘上 scale 得到放大后的左上角坐标
let x = e.clientX - imgRect.left
let y = e.clientY - imgRect.top
let left = (x - scaleViewRect.width / scale / 2)
let top = (y - scaleViewRect.height / scale / 2)
let scaleLeft = left * scale
let scaleTop = top * scale
scalePic.style.left = Math.min(Math.max(-scaleLeft, minLeft), 0) + 'px'
scalePic.style.top = Math.min(Math.max(-scaleTop, minTop), 0) + 'px'
// 设置一个额外的 fixed 的块显示放大区域
// 之前的是相对位置坐标,需要转换为页面坐标 (+imgRect.left)
hoverBlock.style.left = (Math.max(Math.min(left, imgRect.width - scaleViewRect.width / scale), 0)) + 'px'
hoverBlock.style.top = (Math.max(Math.min(top, imgRect.height - scaleViewRect.height / scale), 0)) + 'px'
hoverBlock.style.width = scaleViewRect.width / scale + 'px'
hoverBlock.style.height = scaleViewRect.height / scale + 'px'
})
imgContainer.addEventListener('mouseleave', () => {
scaleContainer.style.display = 'none'
hoverBlock.style.display = 'none'
})#img-container {
width: 200px;
height: 200px;
position: relative;
& img {
object-fit: cover;
}
}
#scale-container {
width: 100px;
height: 100px;
overflow: hidden;
position: absolute;
display: none;
z-index: 100;
left: 100%; /* 紧贴右侧 */
top: 0;
margin-left: 5px; /* 间距 */
}
#scale-pic {
position: absolute;
transform: scale(2);
transform-origin: left top;
left: 0;
right: 0;
}
#hover-block {
position: absolute;
border: pink 1px solid;
box-sizing: border-box;
background-color: #fafafaaa;
display: none;
z-index: 100;
transform-origin: left top;
}5.1.5. 特殊节点的方法属性
- 在特殊类型的节点上,可以使用一些特殊方法属性
FormData对象用于处理表单数据append(name, value[, fileName]):追加数据项,如果第二参数是Blob对象或File对象,则可以设置文件名,Blob类型的默认名称是blob,File对象的默认名称是该文件的名称delete(name):删除数据项get(name):获取对应名称的第一个数据项getAll(name):获取对应名称的所有数据项has(name):判断数据项是否存在set(name, value[, fileName]):设置数据项,如果存在覆盖values():获取所有数据项的值迭代器
| 名称 | 描述 | 示例用法 |
|---|---|---|
src | 图像源URL,可动态设置 | img.src = 'new.jpg'; |
alt | 替代文本,用于无障碍和SEO | img.alt = '描述'; |
width / height | 显示宽度/高度(像素),不影响原图比例 | img.width = 300; |
naturalWidth / naturalHeight | 原图宽度/高度(只读) | console.log(img.naturalWidth); |
complete | 加载完成布尔值(true/false) | if (img.complete) { ... } |
currentSrc | 当前实际加载的源(考虑 srcset) | console.log(img.currentSrc); |
crossOrigin | 跨域设置('anonymous'/'use-credentials') | img.crossOrigin = 'anonymous'; |
decode() | 异步解码图像(现代浏览器,支持WebP等) | img.decode().then(() => { ... }); |
| 名称 | 描述 | 示例用法 |
|---|---|---|
src | 视频源 URL,或使用 <source> 子元素 | video.src = 'video.mp4'; |
autoplay | 自动播放布尔值(需 muted 以符合浏览器策略) | video.autoplay = true; |
controls | 显示浏览器默认控件布尔值 | video.controls = true; |
loop | 循环播放布尔值 | video.loop = true; |
muted | 静音布尔值(播放前必须设置) | video.muted = true; |
preload | 预加载策略('none' / 'metadata' / 'auto') | video.preload = 'auto'; |
currentTime | 当前播放时间(秒,可设置) | video.currentTime = 10; |
duration | 总时长(秒,只读) | console.log(video.duration); |
volume | 音量(0.0 ~ 1.0) | video.volume = 0.5; |
playbackRate | 播放速度(1.0为正常) | video.playbackRate = 2; |
videoWidth / videoHeight | 视频原宽/高(像素,只读) | console.log(video.videoWidth); |
poster | 封面图像 URL | video.poster = 'poster.jpg'; |
play() | 开始播放(返回 Promise) | video.play(); |
pause() | 暂停播放 | video.pause(); |
load() | 重新加载源 | video.load(); |
canPlayType(type) | 检查是否支持媒体类型(返回 '' / 'maybe' / 'probably') | video.canPlayType('video/mp4'); |
requestFullscreen() | 请求全屏(需用户交互) | video.requestFullscreen(); |
| 名称 | 描述 | 示例用法 |
|---|---|---|
src | 音频源 URL,或使用 <source> 子元素 | audio.src = 'audio.mp3'; |
autoplay | 自动播放布尔值(需 muted) | audio.autoplay = true; |
controls | 显示控件布尔值 | audio.controls = true; |
loop | 循环播放布尔值 | audio.loop = true; |
muted | 静音布尔值 | audio.muted = true; |
preload | 预加载策略 | audio.preload = 'metadata'; |
currentTime | 当前时间(秒) | audio.currentTime = 30; |
duration | 总时长(秒) | console.log(audio.duration); |
volume | 音量 | audio.volume = 0.8; |
playbackRate | 播放速度 | audio.playbackRate = 1.5; |
play() | 开始播放 | audio.play(); |
pause() | 暂停 | audio.pause(); |
load() | 重新加载 | audio.load(); |
canPlayType(type) | 检查支持类型 | audio.canPlayType('audio/ogg'); |
| 名称 | 描述 | 示例用法 |
|---|---|---|
src | string - 外部页面 URL。可动态设置 iframe 的加载页面 | iframe.src = 'https://example.com'; |
srcdoc | string - 内联 HTML 内容字符串。优先级高于 src | iframe.srcdoc = '<h1>Hello iframe</h1>'; |
name | string - iframe 的名称,可用于 window.frames[name] 或表单目标 | iframe.name = 'userFrame'; |
sandbox | DOMTokenList - 空格分隔的限制列表。可用 add / remove / toggle 修改。可选值:allow-scripts、allow-same-origin、allow-forms 等 | iframe.sandbox.add('allow-scripts'); iframe.sandbox.remove('allow-forms'); |
allow | string - iframe 内允许使用的功能。可包含多个权限关键字,如 camera、microphone、fullscreen、payment | iframe.allow = 'camera; microphone; fullscreen'; |
allowFullscreen | boolean - 是否允许 iframe 内容进入全屏模式 | iframe.allowFullscreen = true; |
allowPaymentRequest | boolean - 是否允许 iframe 使用 Payment Request API | iframe.allowPaymentRequest = true; |
referrerPolicy | string - 设置加载 iframe 时的 Referer 策略。可选值:"no-referrer"、"origin"、"origin-when-cross-origin"、"strict-origin" 等 | iframe.referrerPolicy = 'no-referrer'; |
loading | "lazy" / "eager" - 控制加载策略,lazy 延迟加载,eager 立即加载 | iframe.loading = 'lazy'; |
credentialless | boolean - 是否加载内容时不携带凭证(cookies / storage) | iframe.credentialless = true; |
fetchPriority | "high" / "low" / "auto" - 提示浏览器加载优先级 | iframe.fetchPriority = 'low'; |
importance | "high" / "low" / "auto" - 指定加载的重要性提示 | iframe.importance = 'low'; |
csp | string - 内容安全策略(CSP),用于控制 iframe 内部资源加载规则 | iframe.csp = "default-src 'self'; script-src 'self'"; |
width | string - iframe 宽度(像素或百分比) | iframe.width = '600'; |
height | string - iframe 高度 | iframe.height = '400'; |
contentWindow | Window - iframe 内部全局 window 对象,只读。同源时可访问 | const win = iframe.contentWindow; |
contentDocument | Document - iframe 内部文档对象,只读。同源时可访问 | const doc = iframe.contentDocument; |
5.2. 事件
- 事件是
js应用跳动的心脏,事件是某些事情发生的信号,可以为事件绑定处理逻辑,事件发生对应的处理逻辑就发生了,让网页动起来- 事件可能是用户的操作,比如点击、移动、键盘输入等;还可能是浏览器行为,比如加载、滚动、重绘、重排等
- 可以监听特定事件,并规定让某些新事件发生以回应这些事件
- 事件一般情况下的作用:
- 验证响应用户行为,比如验证输入信息
- 增加页面动画效果
- 增强用户体验度
5.2.1. Event
Event对象:是事件类型的对象,所有事件都继承自这个接口,是操作事件的基础,在事件处理的过程中可以获取到这个对象,当然也可以人为创建new Event(type, options)type:是字符串类型,表示所创建事件的名称字符串options:可选,是EventInit类型的字典,接受以下字段:bubbles:默认值为false,表示该事件是否冒泡(实际上大多数事件都是冒泡的)cancelable:默认值为false,表示该事件能否被取消composed:默认值为false,指示事件是否会传递到影子根节点之外
可以通过
composedPath()获取到调用到当前节点的冒泡路径数组(EventTarget数组),从调用节点向外,最后到window,如果影子根封闭,外部无法得到影子根内部节点信息
Event实例方法- 阻止默认行为
preventDefault():告诉用户代理此事件被显式处理,它默认的动作不应该照常执行(例如页面滚动、链接跳转、复选框点击或粘贴文本) - 阻止其他监听:
stopPropagation():方法阻止捕获和冒泡阶段中当前事件的进一步传播到下一层节点stopImmediatePropagation():阻止当前节点其他同类型监听和后续传播
事件处理的阶段
事件处理经过三个阶段:捕获、目标、冒泡
- 捕获阶段:事件从
window开始,然后document向最内层元素传递,直到事件到达事件触发目标event.target,有的处理函数会在捕获阶段触发 - 目标阶段:监听器会按创建顺序被调用,激活目标节点事件非捕获阶段处理函数
- 冒泡阶段:事件向外反向传播,触发非捕获阶段的所有非捕获阶段触发器,直到回到
window- 事件冒泡是事件在元素内部向元素外部传播的过程,描述了浏览器如何处理针对嵌套元素的事件
- 点击一个嵌套容器内的元素时,事件会从最内层元素开始传播,直到最外层元素
document再回到window为止,依次触发这些元素上的对应事件 - 事件冒泡可以用于事件委托,在父元素上注册事件,就可以在所有子元素上触发事件时启动处理逻辑,由于关注的子元素可能也有子元素,事件本身的触发元素可能不是关注的子元素,可以使用
e.target.closest(selector)获取到真正关心的子元素,再处理 - 事件冒泡有时是不希望的,可以通过
event.stopPropagation()阻止事件冒泡,更外层的父元素将不会收到事件
# 以点击 button 为例 捕获阶段:window -> document -> html -> body -> div -> button 目标阶段:button 冒泡阶段:button -> div -> body -> html -> document -> window- 阻止默认行为
document和window对象都可以处理冒泡事件,在window上还可以处理浏览器级的事件,这些事件只在window对象上触发;document一般负责处理DOM的事件委托Event实例的常用属性- 创建时传入的内容
type、bubbles、cancelable、composed target:只读属性,是对事件被触发到的对象的引用,即触发事件的元素currentTarget:只读属性,用于标识事件处理器被附加的元素,也就是当前元素isTrusted:用户触发为true,由脚本触发为falsetimestamp:触发时间戳,时间原点是事件创建时刻,为了防止用于识别用户,精度有2ms误差defaultPrevented:表示preventDefault()是否被调用过
部分属性只能在事件处理函数还存在时访问到,比如
currentTarget,因此在requestAnimationFrame中,currentTarget可能为null,建议在使用requestAnimationFrame前,预存所有需要使用的属性引用- 创建时传入的内容
自定义事件
CustomEvent接口:继承自Event,用于创建自定义事件,创建自定义事件时,需要传入事件名称,然后通过new CustomEvent(type, options)创建对象,然后dispatchEvent()触发options中除了Event构造函数支持的属性外,还支持detail,一般将自定义信息挂载在detail属性中- 监听和触发的细节见事件处理
// 创建 cost event = new CustomEvent('my-event', { detail: {name: 'my-name'} }) // 监听 document.addEventListener('my-event', (e) => {console.log(e.detail.name)}) // 触发 document.dispatchEvent(event)
5.2.2. 浏览器事件
- 主要的事件基本上可以分为这些种类,具体可以在MDN查看,在处理事件时会接收到这些类型的事件,可以获取对应的属性
模拟用户操作的节点函数,比如
click()会触发对应的事件(只要事件监控到的状态改变),当然也可以直接dispatchEvent()触发,但在事件处理逻辑中要避免重复触发
- 主要是用户鼠标或键盘输入事件,可以放在所有页面元素上监听区域内操作
| 事件类型 | 说明 | 直接接口 | 继承自 |
|---|---|---|---|
| 鼠标事件 | 鼠标点击click、双击dblclick、移动mousemove(高频触发)、进入mouseenter、离开mouseleave、按下mousedown、抬起mouseup等事件 | MouseEvent | UIEvent |
| 指针事件 | 统一指针事件(鼠标、触控笔、触摸),包括按下pointerdown、移动pointermove(高频触发)、释放pointerup、进入元素pointerenter等,支持多指触控、指针捕获和压力感应 | PointerEvent | MouseEvent |
| 触摸事件 | 触摸屏事件(移动端),如touchstart、touchmove(高频触发)、touchend | TouchEvent | UIEvent |
| 滚轮事件 | Wheel鼠标滚轮事件(高频触发) | WheelEvent | MouseEvent |
| 页面滚动 | Scroll页面滚动事件(高频触发) | - | UIEvent |
| 键盘事件 | 键盘按键输入,如按下keydown(高频触发)、按下keypress(已弃用)、抬起keyup | KeyboardEvent | UIEvent |
| 输入法事件 | 输入法(IME)输入过程事件,如开始输入compositionstart、输入变化compositionupdate、确认输入compositionend | CompositionEvent | UIEvent |
| 剪贴板事件 | 剪贴板事件(copy/paste/cut) | ClipboardEvent | Event |
| 拖放事件 | 拖放操作(drag/drop),拖动到元素上dragover(drag和dragover高频触发) | DragEvent | MouseEvent |
| 框选事件 | 光标选择的内容变化selectionchange | - | Event |
UIEvent接口表示简单的用户界面事件,相比Event接口增加的属性不多detail:返回当前环境鼠标点击次数(click和dblclick)、返回鼠标点击次数+1(mousedown和mouseup)、其他事件为0view:返回触发事件的窗口对象,window对象
- 鼠标事件,鼠标事件后缀包括进入/离开区域
over/out(会冒泡,进入子元素会导致出现子元素进入和父元素离开事件)、进入/离开区域enter/leave(不会冒泡,似乎比over/out晚发生),按下/抬起时down/up、移动时move,当然mouse和pointer类型中还有一些特殊事件,也有其他类型的事件用于监控鼠标mouseclick:在mouseup事件之后触发
pointerpointerdown:只有第一个按键被按下时才会触发,这一点和mousedown不同(每个按键都触发)pointerup:只有最后的按键抬起时才会触发,这一点和mouseup不同(每个按键都触发)pointercancel:当浏览器认为当前不太可能有指针事件(当前指针过多、开启某些设置忽略误触)或在某次移动设备按下后改为移动视口时触发gotpointercapture/lostpointercapture:当元素获得/丢失指针捕获时触发
scroll:一个通用类型的事件,在元素发生滚动时(包括鼠标滚动、滚动条、js完成)触发,要求元素内可滚动(否则无法触发),还有scrollend事件监听滚动结束- 可以通过
e.target.scrollTop和e.target.scrollLeft获取滚动位置 - 如果是监听页面滚动事件应该挂载在
document对象或window对象上 - 通过设置
document.documentElement.scrollTop可以改变页面滚动位置 - 有滚动结束事件
scrollend,但safari还不支持
- 可以通过
wheel:在元素上滚动滚轮时触发,哪怕元素不可滚动
MouseEvent:可以获取到事件触发时鼠标信息,比如按下情况、位置信息relatedTarget:辅助对象,相对事件的触发对象,比如鼠标移入mouseenter中这个属性得到的是发生鼠标移出的元素- 位置信息类,
X是相对于左侧的水平坐标,Y是相对于顶部的垂直坐标clientX/clientY:只读,返回鼠标在应用程序可视窗口中的位置信息,以可视窗口左上角为原点screenX/screenY:只读,返回鼠标在屏幕中的位置信息,以屏幕左上角为原点pageX/pageY:只读,返回鼠标在页面中的位置信息,以页面顶部左上角为原点offsetX/offsetY:只读,返回鼠标在触发元素中的位置信息,以当前元素的内边距区域左上角为原点
- 按键信息类
botton:本次按下的键位信息对应的数字。0主按键,通常指鼠标左键或click()函数触发时的默认值;1辅助按键,通常指鼠标滚轮中键;2次按键,通常指鼠标右键;3第四个按钮,通常是浏览器后退按钮;4第五个按钮,通常是浏览器的前进按钮bottons:所有被按下的鼠标键位信息数字,每个二进制位代表一个按键。0没有按键或者是没有初始化;1鼠标左键;2鼠标右键;4鼠标滚轮或者是中键;8第四按键;16第五按键
shiftKey/ctrlKey/altKey/metaKey:只读,此时是否按下了shift、ctrl、alt、Meta键(windows键盘的win键和mac键盘的command键)
PointerEvent:继承也很大程度上类似于MouseEvent,但额外提供了其他属性pointerId:只读,返回当前指针的编号,不同指针编号不同pressure:返回当前指针的压力,范围是0到1,0表示没有压力,1表示最大压力width/height:只读,返回指针接触面的宽度,不支持时默认为1tiltX/tiltY:只读,返回触笔的倾斜角度,范围是,正值是向右倾斜,不支持时默认为0setPointerCapture(pointerId)/releasePointerCapture(pointerId):捕获和释放指针,需要传入指针编号,被捕获的指针就像没有离开元素边界,锁定后离开元素也会触发鼠标移动事件,释放前不会再触发所有进入或离开事件,但获取到的pointermove鼠标位置是不断变化的hasPointerCapture(pointerId):判断是否被捕获
TouchEvent:触摸屏幕或触控板,事件可以描述与屏幕的一个或多个接触点,并且包括对检测移动、接触点的增加和移除等的支持touches:包含所有的触摸点的列表,每个触点都是Touch对象,可以获取位置信息targetTouches:包含第一次触摸的点列表changeTouches:包含从上一次到现在,状态发生变化的触摸点列表shiftKey/ctrlKey/altKey/metaKey:只读,此时是否按下了shift、ctrl、alt、Meta键(windows键盘的win键和mac键盘的command键)
WheelEvent:描述了用户滚动操作,有些触控板等也可以进行滚动deltaX/deltaY/deltaZ:返回左右/上下/垂直方向的滚动距离deltaMode:返回滚动距离的单位,0表示像素,1表示行高,2表示页,这个单位不同浏览器可能不同
- 键盘事件:键盘相关的事件
keyup/keydown事件虽然可以挂载在所有元素上,但实际上需要元素被聚焦时才能触发,普通元素需要在标签添加tabindex属性(可通过tab或点击聚焦,数字表示tab切换优先级,越高越优先)
KeyboardEvent:描述了用户与键盘的交互,可以获取到按键和行为信息,每个键会单独触发事件,有以下实例属性和方法key:只读,按键名称字符串,所有按键名称见键盘按键key名称code:按键对应代码,未广泛使用,见键盘事件code名称repeat:只读,按键是否重复触发,用于keydown事件时检测是否是第一次触发(keydown事件在一直按着键时会不停触发)shiftKey/ctrlKey/altKey/metaKey:只读,此时是否按下了shift、ctrl、alt、Meta键isComposing:只读,是否正在输入法输入中,也就是事件是否在compositionstart事件触发后和compositionend事件触发前getModifierState(key):获取指定键是否被按下(按下为true),传入按键名称字符串
CompositionEvent:描述了用户输入法输入过程中的信息,相关事件包括开始输入法输入compositionstart、输入过程中组合文本变化compositionupdate、选择了某个文本结束compositionend,文本框输入过程中每次update后还会触发input输入事件,第一次输入和最后一次选择需要键入文本也会触发update事件data:返回引发相关输入事件的字符,对于start事件是空字符串、update事件是当前的字符串,对于end事件是选择的文本字符串
ClipboardEvent:描述了用户剪贴板操作,比如剪贴、复制、粘贴,可以获取到剪贴板信息clipboardData:只读,返回剪贴板信息,一个DataTransfer对象,在cut和copy可以设置复制的字符串信息setData(str);在paste可以获取剪贴板信息getData()
在事件中应该使用
event.preventDefault()阻止默认行为,默认在剪切板放入读取信息
拖动事件:可以为标签添加
draggable="true"支持原生的元素拖拽,用户可以点击并拖动元素- 拖动相关的事件都可冒泡
- 主要事件包括:
- 作用
event.target在被拖拽元素上,拖动中drag、拖动结束dragend、拖动开始dragstart,会冒泡 - 作用
event.target在被放置元素(光标指向元素)上,拖动进入可放置区域dragenter、拖动离开可元素dragleave、拖动到可放置元素上dragover、拖动放置完成drop,因为事件冒泡,此时的被放置元素可能为子元素
- 作用
- 拖动实际上也可以通过鼠标事件完成
DragEvent:拖动过程中产生的事件,继承自MouseEventdataTransfer:拖动动作,一个DataTransfer对象,见DataTransfer- 允许的拖动类型
effectAllowed:拖动的默认光标视觉效果,必须是none,copy,copyLink,copyMove,link,linkMove,move,all,uninitialized之一 - 拖动类型
dropEffect:根据拖动情况选择光标视觉效果,应该在允许的拖动类型中,有移动节点到此位置move、复制一份节点copy、建立节点连接link、被禁止拖放none - 设置拖动影子节点图像
setDragImage(image, x, y):默认是半透明的原节点样式,跟随鼠标移动,x和y是节点或图像相对指针的偏移量 setdata(format, data)/getdata(format)/cleardata(format):设置、获取或删除拖动过程中存放的数据,其中format为数据格式比如text/plain,data为数据内容files:如果拖拽的是文件,获取到拖拽的文件列表
- 允许的拖动类型
drag拖动指南:- 拖动元素必须设置
draggable="true"属性 - 监听拖拽开始
dragstart事件挂载需要拖动的元素或元素父容器上,在此时记录拖动的标签(比如dataTransfer.setData传入id等唯一标识,或通过外部变量暂存节点引用)- 设置被拖拽元素拖动时的样式(此时设置的样式会应用到被拖动的虚拟节点上),可选设置需要支持的视觉效果
effectAllowed - 如果页面上有很多可拖放元素和容器,希望只有部分元素可以拖放到此容器,可以在拖动开始时通过
setData设置唯一标识
- 拖拽到目标容器
- 监听目标容器的
dragenter事件,添加可放置的目标容器样式,并在dragleave事件中还原 - 由于事件会从子元素冒泡,需要设置计数器将
leave和enter相对次数记入,只有在第一次进入目标和抵消时才添加删除样式
- 监听目标容器的
- 在目标容器内部拖拽中
- 需要阻止默认行为才能在之后触发
drop dragover事件判断元素位置(通过判断鼠标位置clientX,并通过getBoundRect获取当前元素的位置,判断是应该插入在前面还是后面),并使用appendChild、before等方法添加元素- 通过修改节点样式设置被拖拽到的节点占位样式,可选设置确定的视觉效果
dropEffect
- 需要阻止默认行为才能在之后触发
- 拖拽完成
- 监听目标容器的
dragend事件,删除目标容器样式,并删除元素拖动的样式 - 如果有必要可以在放置完成后监听
drog事件,触发处理逻辑 - 重置拖动过程中的数据,比如被拖动节点和计数器
- 监听目标容器的
拖动例子<div class="drag-box"> <span>Drag 事件</span> <ul id="drag"> <li draggable="true">1</li> <li draggable="true">2</li> <li draggable="true">3</li> <li draggable="true">4</li> <li draggable="true">5</li> </ul> </div> <div class="drag-box"> <span>Pointer 事件</span> <ul id="pointer-drag"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> </ul> </div>.drag-box { width: 150px; float: left; margin-right: 5px; border: 2px solid #fdfd; text-align: center; } ul { list-style: none; margin-block: 0; padding-inline-start: 0; position: relative; } li { width: calc(100% - 10px); height: 30px; margin: 5px; background-color: #4caf50; color: white; border-radius: 5px; cursor: grab; } .dragging { background-color: #ddd; border: 2px dashed #999; box-sizing: border-box; } .dragover { background-color: #ddd; border-color: #733; }const drag = document.getElementById('drag'); const dragObj = { enterCount: 0, draggedItem: null, annimationId: null } // dragstart:拖拽开始,设置拖拽样式 drag.addEventListener('dragstart', (e) => { dragObj.draggedItem = e.target; e.dataTransfer.effectAllowed = "copy"; }); // dragend:拖拽结束,还原样式 drag.addEventListener('dragend', (e) => { drag.parentElement.classList.remove('dragover'); dragObj.draggedItem.classList.remove('dragging'); dragObj.draggedItem = null; dragObj.enterCount = 0; }); // dragenter:拖拽进入目标元素,设置目标元素样式 drag.addEventListener('dragenter', (e) => { dragObj.enterCount++; if (dragObj.enterCount === 1) { dragObj.draggedItem.classList.add('dragging'); drag.parentElement.classList.add('dragover'); } }); // dragover:拖拽进入目标元素,计算并修改元素位置 drag.addEventListener("dragover", function (e) { e.preventDefault(); e.dataTransfer.dropEffect = "copy"; if (dragObj.annimationId) return const target = e.target; const offsetY = e.offsetY; console.log(e.target, dragObj.draggedItem, drag) dragObj.annimationId = requestAnimationFrame(() => { dragObj.annimationId = null; if (target !== drag) { dragObj.draggedItem.classList.add('dragging'); if (offsetY > target.offsetHeight / 2) { drag.insertBefore(dragObj.draggedItem, target.nextSibling); } else { drag.insertBefore(dragObj.draggedItem, target); } } }) }) // dragleave:拖拽离开目标元素,还原元素位置和拖拽容器样式 drag.addEventListener('dragleave', (e) => { dragObj.enterCount--; if (dragObj.enterCount === 0) { drag.parentElement.classList.remove('dragover'); dragObj.draggedItem.classList.remove('dragging'); } }); const point = document.getElementById('pointer-drag'); const pointObj = { draggedItem: null, placeholder: null, eleOffWidth: 0, offsetX: 0, offsetY: 0, annimationId: null } // pointerdown:拖拽开始,设置拖拽样式 point.addEventListener('pointerdown', (e) => { e.preventDefault(); if (e.target !== e.currentTarget && e.target !== pointObj.placeholder) { pointObj.draggedItem = e.target; pointObj.eleOffWidth = e.target.offsetWidth + 'px'; pointObj.placeholder = e.target.cloneNode(true); pointObj.placeholder.classList.add('dragging'); // 为了快速定位指针指向的需要换位的元素,让指针穿透元素 pointObj.draggedItem.style.pointerEvents = 'none'; // 因为当前指针会穿透拖拽元素,需要重新设置拖拽光标样式 // 虽然取消本身光标样式,但可以通过父元素影响光标样式 point.style.cursor = 'grabbing'; // 放在后面,避免将当前绝对元素位置下移 point.insertBefore(pointObj.placeholder, e.target.nextSibling); // 设置可跟随 e.target.style.position = 'absolute'; e.target.style.zIndex = '9999'; e.target.style.width = pointObj.eleOffWidth; const elementStyle = window.getComputedStyle(pointObj.draggedItem) pointObj.offsetX = e.clientX - e.target.offsetLeft + parseFloat(elementStyle.marginLeft) + parseFloat(elementStyle.borderLeft); pointObj.offsetY = e.clientY - e.target.offsetTop + 2 * parseFloat(elementStyle.marginTop) + parseFloat(elementStyle.borderTop); pointObj.draggedItem.style.left = (e.clientX - pointObj.offsetX) + 'px'; pointObj.draggedItem.style.top = (e.clientY - pointObj.offsetY) + 'px'; // 锁定指针,不需要在移出容器时修改样式 point.setPointerCapture(e.pointerId); } }) // pointerend:拖拽结束,还原样式 point.addEventListener('pointerup', (e) => { point.insertBefore(pointObj.draggedItem, pointObj.placeholder); point.removeChild(pointObj.placeholder); point.releasePointerCapture(e.pointerId); pointObj.draggedItem.style.position = null; pointObj.draggedItem.style.left = null; pointObj.draggedItem.style.top = null; pointObj.draggedItem.style.zIndex = null; pointObj.draggedItem.style.width = null; pointObj.draggedItem.style.pointerEvents = null; pointObj.draggedItem = null; // document.body.style.cursor = null; point.style.cursor = null; }) // pointermove:拖拽移动,计算并修改元素位置 point.addEventListener('pointermove', (e) => { if (pointObj.annimationId) return const { clientX, clientY } = e; pointObj.annimationId = requestAnimationFrame(() => { pointObj.annimationId = null; if (pointObj.draggedItem) { // 调整被移动元素位置 pointObj.draggedItem.style.left = (clientX - pointObj.offsetX) + 'px'; pointObj.draggedItem.style.top = (clientY - pointObj.offsetY) + 'px'; const element = document.elementFromPoint(clientX, clientY); // 由于锁定,在ul外也能触发事件,所以需要判断 if (element !== point && point.contains(element) && element !== pointObj.placeholder) { if (clientY > element.getBoundingClientRect().top + element.offsetHeight / 2) { point.insertBefore(pointObj.placeholder, element.nextSibling); } else { point.insertBefore(pointObj.placeholder, element); } } } }) })- 拖动元素必须设置
- 框选事件
- 选中内容变化
selectionchange:此事件需要挂载在document上,页面光标选择变化时触发,具体获取选中内容的方法,可见Selection API
- 选中内容变化
| 事件类型 | 说明 | 直接接口 | 继承自 |
|---|---|---|---|
| 表单构建 | 表单本身数据被构建时触发(formdata) | FormDataEvent | Event |
| 表单提交 | 表单本身提交submit,在表单提交前触发,可调用 preventDefault()或使回调返回false阻止提交 | SubmitEvent | Event |
| 表单重置 | 表单本身重置reset,在表单重置前触发,可调用 preventDefault()或使回调返回false阻止重置 | - | Event |
| 文本输入 | 表单文本输入实时变化(input)、输入前(beforeInput),每次输入字符都会触发 | InputEvent | UIEvent |
| 值变更 | 表单值变更change,文本框失焦且值变化后触发,选择框或复选框值变更时立即触发 | - | Event |
| 焦点切换 | 元素获得focus或失去焦点blur时触发,有对应的冒泡版本focusin、focusout | FocusEvent | UIEvent |
- 表单上的事件
FormDataEvent:表单数据构建事件formData属性:表单数据对象FormData,包含表单数据
submitEvent:表单提交事件,可能通过input:text标签回车或button:submit标签点击提交时触发submitter属性:type="submit"提交按钮元素,如果是多个button:submit,点击触发时返回点击的元素,回车触发时返回表单中第一个button:submit元素
reset事件没有专门的接口,在button:reset点击时触发,值将重置到初始状态
formdata事件,submit事件和reset事件只能给表单对象绑定 - 表单项的事件
change:在值改变时触发,是基本的Event对象,在获取修改后值时,可以通过event.target获取- 普通的
input、select、textarea、radio对象,内容在value input[type='checkbox']对象,选择结果在checkedinput[type='file']对象,文件列表在files
- 普通的
FocusEvent:焦点变化事件,通常是由输入框触发,也可以是有tabIndex属性的元素触发- 事件包含一个辅助属性
relatedTarget,表示对应的获取或丢失焦点的元素(如果不存在或页面切换后为null) blur/focusout:输入框失去焦点时触发,focusout会冒泡focus/focusin:输入框获得焦点时触发,focusin会冒泡- 事件顺序:
blur、focusout、focus、focusin
- 事件包含一个辅助属性
InputEvent:文本输入实时变化事件,通常由输入框触发,可获取输入内容isComposing:只读,是否正在输入法输入中,也就是事件是否在compositionstart事件触发后和compositionend事件触发前data:返回插入的单个字符串,如果是删除等更改,值为nullinputType:输入的类型,列表可以在w3c文档中
- 主要包括页面生命周期、窗口交互与布局变化、导航和历史变化
| 事件类型 | 说明 | 直接接口 | 继承自 |
|---|---|---|---|
| 页面加载 | 页面加载完成(load)、页面即将卸载(beforeunload) | - | Event |
| 页面显示/隐藏 | 页面显示/隐藏(pageshow/pagehide),用于往返缓存(Back-Forward Cache) | PageTransitionEvent | Event |
| 历史状态变化 | 浏览器历史状态变化(popstate,history.pushState和history.replaceState不会触发) | PopStateEvent | Event |
| 路由变化 | URL 哈希(#)变化(hashchange) | HashChangeEvent | Event |
| 错误 | 资源或脚本错误(error),可捕获加载失败或运行时异常 | ErrorEvent | Event |
| 存储变化 | 通常是 localStorage 数据变化(storage),跨标签页触发;sessionStorage数据变化仅会在同页面的iframe中触发 | StorageEvent | Event |
| 数据库更新 | IndexedDB 数据库版本更新(versionchange) | IDBVersionChangeEvent | Event |
| 页面可见性变化 | 页面可见性变化(visibilitychange),例如切换标签页或最小化窗口 | - | Event |
| 窗口大小变化 | 浏览器窗口大小变化(resize),常用于响应式布局 | - | UIEvent |
| 全屏切换 | 进入或退出全屏(fullscreenchange),document.fullscreenElement 变化时触发 | - | Event |
| 全屏错误 | 进入或退出全屏失败(fullscreenerror) | - | Event |
| 页面打印 | Event实例,打印开始前beforeprint、开始打印或关闭打印预览后afterprint,一般打印样式处理可以直接在@media print完成 | - | Event |
页面生命周期:chrome文档
load:在相应的资源都加载完成后触发,此事件只能挂载在有资源加载的标签(img、audio、video、script、link等)或window对象上,window.onload在所有元素加载完成后触发,而document.DOMContentLoaded在DOM树渲染完成触发beforeunload:在页面即将卸载时触发,可使用preventDefault寻求关闭确认,开启一个浏览器默认弹窗显示提示信息,safari浏览器不支持此事件,考虑使用pagehide事件代替
window.addEventListener("beforeunload", (event) => { // 页面清理逻辑等操作 console.log("beforeunload"); // 标准表明在此阻止默认事件函数会提供关闭确认弹窗 event.preventDefault(); // 部分浏览器需要事件提供返回值 event.returnValue = ""; // 在此函数中直接调用 alter 等弹窗无效 });页面可见性变化
visibilitychange:此事件需要挂载在document对象上,会在对象的visibilityState属性变化时触发visibilityState:只读,当前可见性状态,可选值有visible(页面处于前景,且未锁屏,用户可见)、hidden(页面被切换或系统已锁屏)、prerender(页面渲染中,开始状态)- 典型用法是在页面不在前景时禁止某些活动
- 早期的替代是在
window上的blur事件和focus事件,基于是否点击当前页面来判断
此事件用于判断页面处于前景还是后台,还可以结合
document.hasFocus属性判断页面是否有输入的焦点全屏
fullscreenchange:全屏状态变化时触发- 此时可以通过
document.fullscreenElement获取当前全屏元素,无元素全屏时此值为null
- 此时可以通过
网页地址哈希值变化
hashchange:在网址#后的锚点信息被修改时触发- 一般由
location.hash属性修改时触发,如果修改前后值相同,不会触发 HashChangeEvent接口包含一个oldURL和newURL属性,用于获取当前和旧URL
- 一般由
历史状态变化
popstate:在用户操作前进后退或history.go等前进后退操作时触发,history.pushState、history.replaceState方法不触发popstate事件接口PopStateEvent包含一个state属性,用于获取当前历史状态对象,此对象通过history接口的history.pushState、history.replaceState方法设置,如果是不是这两个方法添加的历史,state为null
历史记录堆栈中页面条目变化处理的基本流程
- 如果新页面不包含当前文档,即有刷新,新文档会被创建,这个过程包括会向新文档的
window发送DOMContentLoad、load事件,当然新文档加载是异步的,下面的步骤会继续执行 - 如果当前页面条目的
title是通过pushState设置的,则新页面的title属性会设置为当前页面的document.title属性 - 如果新页面和旧页面的
Document对象不同,修改此属性 - 新页面中表单设置了
autoComplete属性,则表单会按规则被重置(清空或恢复初始状态) - 当新文档完成了构建,也就是
readyState为complete时让新文档可见,并触发pageshow事件,此时的persist属性为true,表明来自缓存 - 文档的
URL被修改为新值 - 如果文档替换,比如
navigator({replace:true})和location.replace(url),这本页面条目替换 - 如果有锚点
#且无滚动的历史信息,则将文档滚动到锚点 - 新页面的条目设置为当前条目
- 如果有序列化的历史状态信息,将信息恢复,否则
history.state为null - 如果
state发生改变,触发popstate事件 - 如果浏览器希望恢复页面状态,比如滚动位置和表单内容,在此时恢复
- 如果原文档和现文档的文档相同,仅
URL中的锚点片段不同,触发hashchange事件
error用于捕获全局的资源加载错误和代码执行错误,必须挂载在window对象或有资源的元素对象上- 资源加载错误不会冒泡,因此需要设置
addEventListener第二个参数为true来监听 - 不会捕获
promise的错误,这种错误需要使用promise的catch方法或unhandledrejection事件捕获 ErrorEvent接口包含以下属性:message:错误信息filename:错误文件名lineno:错误行号colno:错误列号error:错误对象
- 资源加载错误不会冒泡,因此需要设置
unhandledrejection是promise的错误处理事件,必须挂载在window对象上,得到的是PromiseRejectionEvent包含reason:错误对象promise:错误对象对应的promise对象
| 事件类型 | 说明 | 直接接口 | 继承自 |
|---|---|---|---|
| 网络进度 | 网络加载进度(XHR、FileReader,高频触发) | ProgressEvent | Event |
| 消息通信 | 跨文档、Worker、Socket 消息通信(message) | MessageEvent | Event |
| 连接关闭 | WebSocket 连接关闭 | CloseEvent | Event |
| 服务请求捕获 | Service Worker 捕获请求 | FetchEvent | ExtendableEvent |
| 支付请求更新 | 支付请求更新 | PaymentRequestUpdateEvent | Event |
| 字体加载完成 | 字体加载完成(Font Loading API) | FontFaceSetLoadEvent | Event |
- 消息通信事件
message:主要是跨文档iframe通信- 通信方使用
postMessage(message, url)方法发送消息 - 接收方利用
message事件监听消息 MessageEvent接口包含以下属性:data:发送的消息数据origin:发送方地址
- 通信方使用
| 事件类型 | 说明 | 直接接口 | 继承自 |
|---|---|---|---|
| 加速度计 | 加速度传感器(高频触发) | DeviceMotionEvent | Event |
| 设备方向 | 方向传感器(陀螺仪,高频触发) | DeviceOrientationEvent | Event |
| 游戏手柄 | 游戏手柄连接/断开 | GamepadEvent | Event |
| HID输入报告 | WebHID 设备输入报告 | HIDInputReportEvent | Event |
- 此类中的事件应该绑定在特定对象上使用,如:
MediaStream、WebRTC、WebGLContext
| 事件类型 | 说明 | 直接接口 | 继承自 |
|---|---|---|---|
| 轨道事件 | 媒体流轨道(WebRTC/MediaStream) | TrackEvent | Event |
| 数据通道事件 | WebRTC 数据通道建立事件 | RTCDataChannelEvent | Event |
| ICE候选事件 | ICE 候选者生成事件 | RTCPeerConnectionIceEvent | Event |
| 离线音频完成 | Web Audio API 离线渲染完成 | OfflineAudioCompletionEvent | Event |
| WebGL上下文事件 | WebGL 上下文丢失或恢复 | WebGLContextEvent | Event |
| 插槽更新事件 | slotchange | - | Event |
5.2.3. 事件处理
所有的节点对象
Element、document、window都继承了EventTarget接口,表明可以接受和处理事件,其中有三个实例方法addEventListener(type, listener, options):添加特定类型的事件监听type:事件类型字符串listener:事件处理函数(e)=>{},可以获取到Event对象,e.target可以获取到触发事件的元素,不同的事件实现能够获取到其他相关的数据- 如果
listener是一个函数引用对象,在之前添加过,不会被重复添加
- 如果
options:可选,是AddEventListenerOptions类型的字典,接受以下字段:capture:默认值为false,表示是否提前到捕获阶段处理事件once:默认值为false,表示是否只处理一次,为true在第一次处理后自动移除passive:默认值为false,表示是否允许listener在事件处理过程中阻止默认行为preventDefault(),如滚动或链接跳转signal:可选,是一个AbortSignal对象,表示取消事件的信号,如果取消信号被触发,则事件处理程序将被移除
还可以使用
boolean作为options值,对应capture选项
removeEventListener(type, listener):移除某个监听函数type:事件类型字符串listener:之前使用的事件处理函数
dispatchEvent(e):触发某个事件,传入一个Event对象
标签的内联事件处理器
常见的元素事件几乎都有对应的
on属性,比如onclick等,这些属性接受一个事件处理函数,可以直接将函数提供,进行监听,这来源于内联事件处理器属性onclick="click()"- 现在不推荐直接在
html中定义内联事件处理器,html和js代码混在一起较难阅读 - 这个属性可以赋值,提供唯一的事件处理器,如果被修改后,之前的逻辑就被替换
function click() {} button.onclick = click;
5.2.4. 观察器
观察器
Observer,用于异步监听节点对象属性或节点对象属性下的子节点对象属性的变化,并执行相应的回调函数,获取到所有满足要求的元素,因为元素不太容易同时触发事件,一般每次回调列表(回调参数1)中只有一个元素,第二个参数是观测器本身IntersectionObserver:检测元素是否进入或离开视口或某个元素的可视区域,当有元素超过阈值触发回调
const observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { console.log(entry.target, '进入视口'); console.log('可见比例:', entry.intersectionRatio); // 得到的信息类似 getBoundingClientRect() console.log('可见区域信息:', entry.intersectionRect); console.log('目标元素信息:', entry.boundingClientRect); console.log('观察区域信息:(root)属性', entry.rootBounds); // 监听对象创建到现在的时间 console.log('时间:', entry.time); } }); }); observer.observe(document.querySelector('#target'), { root: document.querySelector('#container'), // 认为可见的观察区域,默认 null 时为视口 rootMargin: '0px', // 相对观察区域的扩展(+)、收缩(-) threshold: 0.5, // 触发回调时的阈值,可以是个列表 });MutationObserver:监听DOM结构的变化,例如节点的增删、属性变化、文本内容变化等
const observer = new MutationObserver((mutations, observer) => { mutations.forEach(mutation => { console.log('变动类型:', mutation.type); console.log('变动目标:', mutation.target); }); }); observer.observe(document.body, { childList: true, // 监听子节点增删 attributes: true, // 监听属性变化 attributeFilter: ['class'], // 监听的属性列表 characterData: true, // 监听文本内容变化 subtree: true // 监听整个子树 });ResizeObserver:监听某个元素的大小变化
const observer = new ResizeObserver((entries, observer) => { for (const entry of entries) { // contentRect: 获取当前元素大小位置 // 来自旧的事件规范 console.log('新的大小:', entry.contentRect.width, entry.contentRect.height); console.log('元素:', entry.target); } }); observer.observe(document.querySelector('#box'));PerformanceObserver:异步监听浏览器的性能指标
const observer = new PerformanceObserver((list, observer) => { for (const entry of list.getEntries()) { console.log(entry.name, entry.duration); } }); // 'resource': 资源加载信息 // 'paint': 首次绘制(FP, FCP) // 'longtask': 主线程长任务 // 'navigation': 页面加载阶段信息 // 'largest-contentful-paint': 最大内容绘制 observer.observe({ entryTypes: ['resource', 'paint', 'longtask'] });监控步骤
- 创建一个观察器实例,传入
(callback, options) - 使用
observe(element)添加监听元素 - 使用
unobserve(element)移除监听元素 - 使用
disconnect()取消所有监听
- 创建一个观察器实例,传入
5.3. canvas
canvas是HTML中的画布标签,一个简单的canvas标签如下:- 通常需要一个
id属性,用于获取到这个节点对象 - 绘图宽高属性,用于设置画布的宽高,默认是
300px宽100px高 - 虽然也可以使用
CSS设置宽高,但canvas标签在绘制时图像会伸缩以适应它的框架尺寸,如果CSS的尺寸与初始画布的比例不一致,它会出现扭曲,最好明确宽高
// 不一致的宽高会带来一个缩放比例 const scaleX = canvas.width / rect.width; // 处理CSS缩放与实际像素比例 const scaleY = canvas.height / rect.height; // 元素尺寸/画布尺寸 = 显示坐标/绘制坐标 // 内部依旧使用 绘图宽高坐标系 // 但显示时的尺寸是画布的宽高 // 如果涉及到尺寸变化,且比例不一致显示就不正确 // 从期望显示坐标到绘图坐标 const x = show.x * scaleX; const y = show.y * scaleY; // 对于高DPI设备,可以考虑修正画布大小,确保显示清晰 const devicePixelRatio = window.devicePixelRatio || 1; canvas.width = canvas.clientWidth * ratio; canvas.height = canvas.clientHeight * ratio; ctx.scale(ratio, ratio); // 修正绘图比例canvas看起来和img很像,除了没有src和alt属性外,img有的属性基本都有,但这些属性不会对内部的绘图产生影响,影响的是画布元素在网页中的显示位置和外观<!-- 开始时将得到一个透明的画布 --> <canvas id="myCanvas" width="300" height="300"></canvas> <!-- 对于 IE9 之前的浏览器兼容,需要在其中添加替换元素 --> <canvas id="clock" width="150" height="150"> <!-- 老版本浏览器不会识别 canvas 标签,导致替换元素显示在界面上 --> <!-- 注意 canvas 需要结束标签,否则之后的内容都不会显示在支持的浏览器上 --> <img src="images/clock.png" width="150" height="150" alt="" /> </canvas>- 通常需要一个
获取
canvas对象进行绘制const canvas = document.getElementById('myCanvas'); // 判断浏览器是否兼容可以查看 canvas 是否有特定的方法 if (canvas.getContext) { const ctx = canvas.getContext('2d'); // 绘制 } else { // canvas-unsupported }
5.3.1. 2D 绘制
画布中的二维栅格(使用
Canvas 2D):以canvas标签的左上角为原点,水平方向为x轴、垂直方向为y轴
二维栅格 可以支持两种形式的图形绘制:矩形和路径(由一系列点连成的线段)
绘制矩形
fillRect(x, y, width, height):绘制一个填充的矩形strokeRect(x, y, width, height):绘制矩形边框clearRect(x, y, width, height):清除矩形区域
function draw() { const canvas = document.getElementById("canvas"); if (canvas.getContext) { const ctx = canvas.getContext("2d"); ctx.fillRect(25, 25, 100, 100); ctx.clearRect(45, 45, 60, 60); ctx.strokeRect(50, 50, 50, 50); } }绘制路径:首先需要确定起始点,然后绘制路径,之后把路径封闭,一旦路径生成,就可以描边或填充颜色
- 开始绘制:
beginPath() - 移动笔触:
moveTo(x, y),移动过程中不会产生绘制线 - 绘制路径
- 绘制从当前位置到指定位置的直线路径:
lineTo(x, y),绘制过程中会生成绘制线 - 绘制弧线:
arc(x, y, radius, startAngle, endAngle, anticlockwise),画一个以(x,y)为圆心的以radius为半径的圆弧(圆),从startAngle开始到endAngle结束,角度需要使用弧度制Math.PI(x轴正方向是0),按照anticlockwise给定的方向(默认为顺时针false)来生成 - 绘制封闭弧线:
arcTo(x1, y1, x2, y2, radius),根据给定的控制点和半径画一段圆弧,再以直线连接两个控制点 - 使用二次贝塞尔曲线:
quadraticCurveTo(cpx, cpy, x, y),绘制一个二次贝塞尔曲线,参数依次为控制点坐标和结束点坐标 - 使用三次贝塞尔曲线:
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y),绘制一个贝塞尔曲线,参数依次为两个控制点坐标和结束点坐标 - 矩形路径:
rect(x, y, width, height)绘制一个左上角坐标为(x,y),宽高为width以及height的矩形,执行完此方法后,笔触会回到(0, 0)
- 绘制从当前位置到指定位置的直线路径:
- 填充
fill()描边stroke()
新的
Path2D对象拥有之前的路径绘制方法,也可以接受svg字符串输入,可以直接作为填充和描边的参数new Path2D(path)function draw() { let canvas = document.getElementById("canvas"); if (canvas.getContext) { let ctx = canvas.getContext("2d"); let rectangle = new Path2D(); rectangle.rect(10, 10, 50, 50); let circle = new Path2D(); circle.moveTo(125, 35); circle.arc(100, 35, 25, 0, 2 * Math.PI); ctx.stroke(rectangle); ctx.fill(circle); let p = p = new Path2D("M10 10 h 80 v 80 h -80 Z"); } }- 开始绘制:
设置填充和描边样式,默认的填充颜色描边颜色就是黑色,可以在填充和描边前通过
ctx对象属性修改,支持符合颜色标准的各种值,见颜色值fillStyle:填充颜色strokeStyle:描边颜色globalAlpha:直接设置透明度
描边相关
lineWidth:描边宽度,默认值为1.0,必须为正数lineCap:设置描边端点样式,它可以为下面的三种的其中之一:- butt:默认是
butt,与在起始点开始,结束点终止 - round:端点是圆弧,在端点处外突,加上了半径为一半线宽的半圆
- square:端点处突出,加上了等宽且高度为一半线宽的方块
- butt:默认是
lineJoin:设置描边连接样式- round:连接边角处被磨圆了,圆的半径等于线宽
- bevel:连接处尖角看起来好像被磨平了
- miter:默认是
miter,保留尖角
描边样式<canvas id="canvas" width="150" height="150"></canvas>function draw() { const ctx = document.getElementById("canvas").getContext("2d"); ctx.lineWidth = 10; ["round", "bevel", "miter"].forEach((lineJoin, i) => { ctx.lineJoin = lineJoin; ctx.beginPath(); ctx.moveTo(-5, 5 + i * 40); ctx.lineTo(35, 45 + i * 40); ctx.lineTo(75, 5 + i * 40); ctx.lineTo(115, 45 + i * 40); ctx.lineTo(155, 5 + i * 40); ctx.stroke(); }); } draw();miterLimit:设置miter时描边斜接限制,值越大越容易被判断相交,线段会连接到预估的相交点上虚线相关
setLineDash(segments):设置虚线样式,指定线段和间隔的交替间隔,虚线从线段开始,数组中的元素会被循环使用,默认为空就是直线lineDashOffset:设置虚线偏移,起始虚线的描点会相对起始点沿绘制方向偏移后开始,支持正数(绘制方向偏移,延后绘制)和负数getLineDash():获取虚线样式
canvas还支持绘制渐变图案,见MDN文档
5.3.2. 图片填充和转换
canvas支持直接将图片绘制到画布上,也可以出画布转换为图片输出ctx.drawImage:绘制图片的函数,有三种重载ctx.drawImage(image, dx, dy):把整张图片原尺寸绘制到画布的(x, y)位置,图片左上角对齐(x, y),位置为负坐标会从画布外开始绘制ctx.drawImage(img, x, y, width, height);:把整张图片缩放到width和height,并把图片绘制到画布的(x, y)位置,图片左上角对齐(x, y)ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight):参数比较复杂,具体如下
ctx.drawImage( img, sx, sy, sw, sh, // 从源图中截取的矩形区域 dx, dy, dw, dh // 绘制到画布上的位置和大小 );第一个参数必须为图片源对象,也就是
HTMLImageElement(<img>)、HTMLVideoElement、HTMLCanvasElement、ImageBitmap、OffscreenCanvas之一toDataURL(type, encoderOption):将画布内容转换为base64编码的type类型的图片字符串,像素为96dpitype:图片类型,默认为image/png,还可以是image/jpeg、image/webpencoderOption:图片质量(0-1区间内的数字),jpeg和webp图片类型时才有效,默认值为0.92
由于生成的是存储在内存的字符串,当图片很大时,可能影响性能,赋值给图片
src可能超过url限制,通常应该优先选择toBlobtoBlob(callback, type, quality):将画布内容转换为blob对象,像素为96dpi,blob对象是一个二进制对象,通常用于保存文件callback:回调函数,获得到的参数为blob对象type:同toDataURL,图片类型,默认为image/pngquality:同toDataURL,图片质量(0-1区间内的数字),jpeg和webp图片类型时才有效,默认值为0.92
const canvas = document.getElementById("canvas"); // 转换为 blob 对象 canvas.toBlob((blob) => { const newImg = document.createElement("img"); const url = URL.createObjectURL(blob); newImg.onload = () => { // 不再需要读取该 blob,因此释放该对象 URL.revokeObjectURL(url); }; newImg.src = url; document.body.appendChild(newImg); });
5.3.3. 3D 绘制
- 三维(使用
WebGL):以canvas标签的中心为原点,水平方向向右为x轴正方向、垂直方向向上为y轴正方向,还有正方向向内的z轴 - 需要通过
canvas.getContext('webgl')获取到webgl对象,使用webgl对象进行绘制
