现代JS学习笔记:DOM及其操作

Posted by Mars . Modified at

学习内容:《现代JavaScript教程》

17 DOM(Document Object Model)

17.1 DOM

DOM将一个页面的所有内容转化为可以被JS获取、修改的对象树,包括页面的根(document)、元素、文本、注释等。

一个HTML页面的DOM树大概是这种结构:

官方代码风格

17.2 DOM节点类

每一个DOM节点根据自身类型的不同,可能具有自身不同的属性。但是他们都遵循一定的继承规律,因而才能共享一些方法属性。

DOM节点的继承关系如下:

官方代码风格

EventTarget — 是根的“抽象(abstract)”类。该类的对象从未被创建。它作为一个基础,以便让所有 DOM 节点都支持所谓的“事件(event)”,我们会在之后学习它。

Node — 也是一个“抽象”类,充当 DOM 节点的基础。它提供了树的核心功能:parentNode,nextSibling,childNodes 等(它们都是 getter)。Node 类的对象从未被创建。但是有一些继承自它的具体的节点类,例如:文本节点的 Text,元素节点的 Element,以及更多异域(exotic)类,例如注释节点的 Comment。

Element — 是 DOM 元素的基本类。它提供了元素级的导航(navigation),例如 nextElementSibling,children,以及像 getElementsByTagName 和 querySelector 这样的搜索方法。浏览器中不仅有 HTML,还会有 XML 和 SVG。Element 类充当更多特定类的基本类:SVGElement,XMLElement 和 HTMLElement。

  • HTMLElement — 最终是所有 HTML 元素的基本类。各种 HTML 元素均继承自它:
  • HTMLInputElement — <input> 元素的类,
  • HTMLBodyElement — <body> 元素的类,
  • HTMLAnchorElement — <a> 元素的类,

……等,每个标签都有自己的类,这些类可以提供特定的属性和方法。

因此,给定节点的全部属性和方法都是继承的结果。

17.3 DOM节点的属性

17.3.1 nodeType

过时的获取DOM节点类型的方法:每一个DOM类型有自己的nodeType值,是一个数字,对于元素节点 elem.nodeType == 1,对于文本节点 elem.nodeType == 3等。

现在可以使用 instanceof 操作符查看节点类型。

17.3.2 nodeName、tagName

获取节点的名称。nodeName支持所有DOM节点,tagName仅支持元素节点。

一般使用nodeName即可。tagName不如nodeName强大。

17.3.3 innerHTML、outerHTML

innerHTML 属性允许将元素中的 HTML 获取为字符串形式,可直接修改。

outerHTML 属性包含了元素的完整 HTML,就像 innerHTML 加上元素本身一样。也可以直接修改。

!注意:

直接修改innerHTML和outerHTML都会造成修改部分的刷新。

即使是使用+=这种部分修改的方法,DOM引擎也会先删除所有原有内容,然后加入修改后的内容。

17.3.4 textContent

textContent 提供了对元素内的文本的访问权限:仅文本,去掉所有 <tags>

修改textContent的内容,如果里面包含类似HTML标签的文本,也会按照文本对待,不会真正地变成HTML。

17.3.5 HTML特性:写在tag标签内可识别的属性

HTML标准规定了各类型元素的一些标准特性(Attribute),在HTML中可以在tag标签内书写,并直接可以通过DOM节点访问(被自动识别)。比如<input>的value特性,<a>的href特性,各元素的id特性等。

HTML特性是大小写不敏感的,而且总是字符串类型的。

注意:

属性Property和特性Attribute的区别:属性Property指的是JS对象内部储存的键值对,无论是标准的还是自定义非标准的。在DOM对象中既可以是HTML标准特性、原生属性也可以是自定义属性,都叫做Property。而特性Attribute指的是HTML元素中标准规定的一些属性,也就是常用于写在Tag内的那些标准属性。

★ HTML标准里规定的特性,都可以被浏览器自动识别,可直接通过DOM元素的同名属性访问。(注意必须是HTML规定的标准特性)

★ 非标准属性,比如自定义属性,不能直接访问,需要通过专用API:

  • DOMelement.hasAttribute(name) — 检查特性是否存在。
  • DOMelement.getAttribute(name) — 获取这个特性值。
  • DOMelement.setAttribute(name, value) — 设置这个特性值。
  • DOMelement.removeAttribute(name) — 移除这个特性。

使用这些方法后,对应DOM对象的属性也会更新。

DOMelement.setAttribute(class, value);
DOMelement.class; //value

17.3.5.1 HTML特性和DOM属性有时存在差异

比如:<input>元素内的checked特性,在使用.getAttribute()方法返回的是空字符串,而使用DOMelement.checked获取的是true布尔值。

<a>标签href=’#here’特性,使用getAttribute()获取到的是字符串’#here’,而DomElement.href获取的是完整的URL。

也就是说,getAttribute()方法会照着HTML中书写的原样返回字符串。而DOM属性会返回JS想要的结果。

17.3.6 非标准的HTML特性:dataset

可以在HTML标签中使用非标准的自定义特性,然后在JS中用getAttribute/setAttribute方法获取与设置。

但是为了防止冲突,预留了以data-开头的特性用于自定义使用。以data-开头的特性,可以在DOM对象中使用DOMelement.dataset中对应属性找到。比如:

<div id=’obj’ data-pool=’swim’></div>

中的data-pool属性,可以使用obj.dataset.pool获取。

data-time-counter这类使用短横线连接的自定义特性名,属性中使用驼峰法获取:dataset.timeCounter

17.3.7 自定义属性

也可以随意为DOM元素添加属性,使用自定义的命名。但需要注意他们是大小写敏感的。比如:可以设置document.body.say = function(){ alert(‘Hey!’) }

17.4 遍历DOM节点

17.4.1 顶层节点<html>、<body>、<head>

  • document.documentElement 对应 <html>节点;
  • document.body 对应 <body>节点;
  • document.head 对应 <head>节点。

17.4.2 DOM节点的遍历属性

如上图所示,每一个DOM节点都有遍历其他节点的方法。这些遍历是在纯DOM节点层面的,里面包含了html的各种类型节点,包括可能不关注的文本节点等。

如果想在纯元素节点之间遍历,使用下面的这些属性。

17.5 DOM对象的创建、修改与插入

17.5.1 创建DOM节点

  • document.createElement(tag): 创建元素节点;
  • document.createTextNode(text): 创建文本节点。

17.5.2 修改DOM节点

使用DOM对象的属性,直接修改新创建DOM对象。

class特性,需要使用domElement.className修改,因为class是JS中保留关键字。

17.5.3 插入DOM节点

以下方法用于插入DOM节点:

  • node.append(…nodes or strings) —— 在 node 末尾 插入节点或字符串,
  • node.prepend(…nodes or strings) —— 在 node 开头 插入节点或字符串,
  • node.before(…nodes or strings) —— 在 node 前面 插入节点或字符串,
  • node.after(…nodes or strings) —— 在 node 后面 插入节点或字符串,
  • node.replaceWith(…nodes or strings) —— 将 node 替换为给定的节点或字符串。

这些方法在参数为字符串时,插入到HTML后不会被识别为HTML代码,而是各种符号都被转义的字符串。如果想要使用字符串形式,为HTML文档动态添加可识别的HTML代码,需要使用DOM对象的.insertAdjacentHTML()方法:

elem.insertAdjacentHTML(where, html)。该方法的第一个参数是代码字(code word),指定相对于 elem 的插入位置。必须为以下之一:

  • “beforebegin” — 将 html 插入到 elem 前插入,
  • “afterbegin” — 将 html 插入到 elem 开头,
  • “beforeend” — 将 html 插入到 elem 末尾,
  • “afterend” — 将 html 插入到 elem 后。

17.5.4 替换DOM节点

DOMelement.replaceWith()可以用于原地替换一个DOM节点。

17.5.5 删除DOM节点

使用DOMelement.remove()方法移除节点。

17.5.6 移动DOM节点

获取并移动一个DOM节点到另一个位置,引擎会自动将原HTML元素删除,然后在新的位置插入。(无需手动删除原节点)。

17.5.7 克隆DOM节点

使用DOMelement.cloneNode( )来克隆一个DOM节点。

  • 当传入参数true时,为深克隆(全部子元素都克隆);
  • 当传入参数False,为浅克隆,克隆将不包含子元素。

17.6 使用DOM修改、获取CSS样式

17.6.1 直接修改style属性(元素样式)

DOM元素自身具有Style属性,可以直接使用JS访问修改。它对应的是HTML中这个元素的Style特性,也只对应HTML中的style特性(无法访问其他形式加入的CSS内容,比如外部样式表)。

elem.style.width="100px" 的效果等价于我们在 style 特性中有一个 width:100px 字符串。

多词属性,需要进行驼峰化处理才能访问。

这种修改方式的特点如下:

  1. 它比通过CSS Class修改的优先级要高(内联),默认会覆盖CSS Class中相同的内容;
  2. style.css只能逐一修改CSS属性(可能造成多次回流、重绘);
  3. 如果需要像定义CSS一样,一次传入一个字符串作为Style,需要使用style.cssText = ;(这样操作会删除所有现有已定义的style特性)
  4. 只有需要复杂计算才能获取的CSS属性,需要通过这样的方式修改。其他情况一般默认应采用CSS Class修改。

17.6.2 创建、修改CSS Class

17.6.2.1 className

早期的JS不允许设置‘class’为对象属性名,因为是保留关键字。所以早期使用className作为获取DOM元素CSS Class的属性名。

className设置的是DOM元素完整的字符串,一旦修改整个class都会被替换。

17.6.2.2 classList

DOM元素还有一个classList属性,它是一个可迭代对象,里面记录着这个DOM元素上绑定的所有CSS Class。它自带四个方法:

  • elem.classList.add/remove(class) — 添加/移除类。
  • elem.classList.toggle(class) — 如果类不存在就添加类,存在就移除它。
  • elem.classList.contains(class) — 检查给定类,返回 true/false。

17.6.3 获取最终CSS样式

使用.style只能获取定义在style特性中的CSS样式,不能获取最终的元素CSS。

使用全局环境下的getComputedStyle(DOMelement,[pseudo])方法,可以返回计算后的(Computed)CSS结果对象,然后按需获取想要的结果。

17.7 DOM元素的各种几何属性

DOM元素自身具有一些反应自身几何性质的属性。这些属性所反应的信息如下图所示。

17.7.1 offset参数

带有offset的参数反应的是元素的外边界相关的参数。(元素的外边界,指的是元素border外的的最外层边界)

  • offsetParent: DOM元素的offset基准元素。offsetParent 是最接近的祖先(ancestor),在浏览器渲染期间,它被用于计算坐标。
    它是下列元素之一:CSS 定位的(position 为 absolute,relative 或 fixed),或 <td>,<th>,<table>,或 <body>。
  • offsetLeft、offsetTop:元素左上角点到offsetParent左上角点的横向、纵向距离;
  • offsetWidth、offsetHeight: 元素外边界的宽度和高度;

17.7.2 client参数

带有client的参数反应的是元素的内边界相关的参数。(元素的内边界,指的是元素border内的不包含滚动条的边界)

  • clientTop、clientLeft: 内边界左上角点到外边界左上角点的水平、垂直距离;(在一般条件的盒子模型中就是边界厚度)
  • clientWidth、clientHeight: 内边界的宽度和高度;

17.7.3 scroll相关参数

是可滚动部分的相关参数。

  • scrollTop、scrollLeft:可滚动内容的上边界到现在内边界上边缘的距离; (已滚动的距离)
  • scrollWidth、scrollHeight: 可滚动部分的总宽度、高度;

17.7.4 为什么不建议从CSS获取元素几何参数?

① CSS中元素的几何参数,还取决于CSS box-sizing属性,因此不准;

② CSS中获取到的几何参数,有可能是’auto’;

③ 因为滚动条的存在,不同浏览器返回的CSS属性有差异。

★ clientWidth与getComputedStyle(elem).width的区别?

  1. clientWidth 值是数值,而 getComputedStyle(elem).width 返回一个以 px 作为后缀的字符串。
  2. getComputedStyle 可能会返回非数值的 width,例如内联(inline)元素的 “auto”。
  3. clientWidth 是元素的内部内容区域加上 padding,而 CSS width(具有标准的 box-sizing)是内部内容区域,不包括 padding。
  4. 如果有滚动条,并且浏览器为其保留了空间,那么某些浏览器会从 CSS width 中减去该空间(因为它不再可用于内容),而有些则不会这样做。clientWidth 属性总是相同的:如果为滚动条保留了空间,那么将减去滚动条的大小。

17.7.5 获取窗口的width/height(布局视口Layout Viewport)

获取整个窗口(可视部分)的width或者height,需要使用document.documentElement 的 clientWidth/clientHeight:

window.innerHeightwindow.innerWidth 都是包含了滚动条的。

document.documentElement.clientWidth是不包含滚动条的。

17.7.6 获取整个文档的width/height

理论上使用document.documentElement.scrollWidth/scrollHeight.

但是,由于种种历史原因,document.documentElement.scrollHeight经常不返回整个文档的高度,而是别的数据。因此必须使用以下方法:

let scrollHeight = Math.max(
  document.body.scrollHeight, document.documentElement.scrollHeight,
  document.body.offsetHeight, document.documentElement.offsetHeight,
  document.body.clientHeight, document.documentElement.clientHeight
);

17.8 DOM元素的坐标

17.8.1 坐标系

大多数 JavaScript 方法处理的是以下两种坐标系中的一个:

  • 相对于视口 — 类似于 position:fixed,从视口的顶部/左侧边缘计算得出。 我们将这些坐标表示为 clientX/clientY,当我们研究事件属性时,就会明白为什么使用这种名称来表示坐标。
  • 相对于文档 — 与文档根(document root)中的 position:absolute 类似,从文档的顶部/左侧边缘计算得出。 我们将它们表示为 pageX/pageY。

17.8.2 获取DOM元素的视口坐标

每个DOM元素都有一个getBoundingClientRect()方法,它返回一个对象(DOMRect对象),里面包含了各种这个元素相对于窗口的坐标属性。

bounding

17.8.3 获取视口中某一坐标的元素

document.elementFromPoint(x,y)可以获取当前窗口的x,y坐标点,嵌套最多的DOM元素。(指哪打哪!)

17.8.4 获取DOM元素的文档坐标

思路是:先获取DOM元素的窗口坐标,然后获取整个窗口的滚动参数(window.pageXOffsetwindow.pageYOffset),然后将两者相加。

17.9 页面滚动

17.9.1 获取当前窗口的滚动参数

window.pageXOffsetwindow.pageYOffset可以获取当前窗口的滚动位置参数。但是这两个参数都是只读的。

17.9.2 直接使用JS代码操作页面滚动

有三种方式可以使用JS操作页面滚动:

  1. 可以通过直接赋值给document.documentElement.scrollTop/scrollLeft来实现页面的滚动;
  2. window.scrollTo(x,y)和window.scrollBy(delta_x,delta_y)可以实现直接滚动到xy和相对当前位置步进滚动delta_x,delta_y的功能。
  3. ★ DOMElement.scrollIntoView(<Boolean>);这个方法可以使任何DOM节点立即跳转到窗口内。(当传入的Boolean为true,节点位于窗口最上方。当为false,在窗口最下方。)

17.9.3 禁止页面滚动

要使文档不可滚动,只需要设置 document.body.style.overflow = "hidden"。该页面将“冻结”在其当前滚动位置上。

document.body.style.overflow = “” 可以恢复滚动。

这个方法滚动条会消失,所以页面会有点变化。

17.10 ★DOM事件

17.10.1 绑定DOM事件的方法

17.10.1.1 HTML标签特性

直接在HTML标签内写onclick、onload这类特性。

这种显式传入的方式,函数尾部应该带有括号。

17.10.1.2 JS获取DOM元素并绑定

用JS获取DOM元素,然后绑定onload、onclick这类方法。(传入函数体,而不是调用的函数)

上面这两种方法会互相覆盖。

DOM事件中This就是发生事件的那个元素本身。

17.10.1.3 addEventListener()

使用addEventListener()可以为DOM元素添加多个事件。

element.addEventListener(event, handler[, options]);
  • event:事件名,例如:”click”;
  • handler:处理程序;
  • options:具有以下属性的附加可选对象:
    • once:如果为 true,那么会在被触发后自动删除监听器。
    • capture:<true/false>元素在捕获还是冒泡阶段发生(true: 捕获, false:冒泡)。由于历史原因,options 也可以是 false/true,它与 {capture: false/true} 相同。
    • passive:如果为 true,那么处理程序将不会调用 preventDefault().

使用removeEventListener()去除DOM元素上绑定的事件。

17.10.2 事件对象

DOM事件发生的时候,会生成一个事件对象event,里面记载了这次事件的一些信息。这个event对象可以通过事件的handler参数进行引用。

17.10.3 使用对象作为处理程序handleEvent

addEventListener()第二个参数不仅可以传入函数作为事件,还可以传入对象obj。

这种情况下,将默认使用obj.handleEvent()作为事件。

为什么要使用对象呢?

因为一个对象内可以定义多个方法,这样每一个单独的功能就能独立出来,单独起一个函数名,这样的代码更直观容易维护、修改。

17.10.4 事件的冒泡和捕获

17.10.4.1 事件冒泡

默认情况下,几乎所有的事件都以冒泡的形式发生。(focus事件不冒泡)

17.10.4.1.1 什么是事件冒泡?

一个元素,它被包裹在它的父级元素中,然后还有更高层的元素包裹。这些元素上可能都被挂载了自己的事件。(比如click)

冒泡的意思,就是当你在一个内部元素上触发事件(click),它先在最内层的元素上发生,然后是上一层父级元素的事件,逐级直到最顶层元素事件完毕。像一个气泡一样,从最下面一直传递到最上面。

17.10.5 阻止浏览器默认行为

浏览器的默认行为,比如点击链接会跳转等,可以被手动取消。总共有两种方法:

  1. event.preventDefault()方法;
  2. 使用on<event>分配事件时,可以返回false表示阻止浏览器默认行为。

17.11 页面生命周期

以下事件按页面生命流程发生。

17.11.1 DOMContentLoaded

此时浏览器已完全加载 HTML,并构建了 DOM 树,但此时像 <img> 和样式表之类的外部资源可能尚未加载完成。

这个事件的使用方法是:

document.addEventListener(‘DOMContentLoaded’, func);

17.11.2 load

浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。

这个事件在window对象上挂载。

window.onload = func;

17.11.3 beforeunload

当用户点击离开,或者试图关闭页面,会触发这个事件函数。

window.onbeforeunload = func;

这里注意:func的返回值有重要意义,func返回False或者一个字符串的时候,浏览器会触发弹出离开前的确认信息。

因为这个功能经常被滥用,现在返回字符串也不会被显示出来,只是当做返回了false。

17.11.4 unload

当访问者离开页面时,window 对象上的 unload 事件就会被触发。我们可以在那里做一些不涉及延迟的操作,例如关闭相关的弹出窗口。

17.11.5 监测页面生命周期变化的方法

document.readyState 记载了页面的加载状态。它有四种取值:

  • uninitialized - 还未开始载入
  • loading - 载入中
  • interactive - 已加载,文档与用户可以开始交互
  • complete - 载入完成

可以在document上加载一个readystatechange监听器,当页面生命周期发生变化,会执行相应函数。

17.12 <script>的同步加载与异步加载

17.12.1 同步加载

<script>默认情况下在页面中是同步加载的,也就是说当浏览器对HTML解析到script标签时,会立即下载+执行里面的代码,整个页面停止加载,进行等待,直到JS下载+执行完毕。

17.12.2 异步加载

defer

17.12.2.1 defer

在Script标签中加入defer 特性,就是告诉浏览器不要等待这个脚本下载。浏览器将继续加载后面的 HTML,构建 DOM。JS脚本会异步下载(与页面加载并发),然后等 DOM 解析完成后,脚本才会执行。

defer标记的脚本,会在DOM解析完成后(解析到</html>),但DOMContentLoaded事件触发前,按照声明的先后顺序执行。

排在后面的defer脚本,即使先加载完成,也等待前面的加载执行完毕后再执行。

17.12.2.2 async

在下载方面,asyncdefer 相同,都是让脚本不阻塞页面加载,异步下载。

但是async标记的script不会等待任何其他DOM元素或Script的加载或执行,它是完全独立的,独立加载+执行,下载完了立即执行,可能发生在页面周期的任何时候

17.12.3 动态脚本加载

在JS中创建script对象,然后添加到页面中的脚本。叫做动态脚本。

动态脚本默认和带有async关键字是一样的:立即加载,按加载完成先后顺序执行,与代码内挂载的顺序无关。

但如果手动设置了async = false,则以挂载先后顺序执行,就是defer。

17.13 其他资源的加载

onload和onerror几乎可以用在带有src特性的任何DOM元素上。

图片 <img>,外部样式,脚本和其他资源都提供了 load 和 error 事件以跟踪它们的加载:

  • load 在成功加载时被触发。
  • error 在加载失败时被触发。

唯一的例外是 <iframe>:出于历史原因,不管加载成功还是失败,即使页面没有被找到,它都会触发 load 事件。

17.14 DOM变动观察器

MutationObserver 是一个内建对象,它观察 DOM 元素,并在检测到更改时触发回调。具体见:Mutation Observer

17.15 事件循环

当浏览器没有任务执行时,处于休眠状态。当任务出现,则按照出现的先后顺序执行任务,先进入的任务先执行。

17.15.1 宏任务

以下内容被称为宏任务,这些任务按照出现的顺序在浏览器内部组成一个序列,按照进入的先后顺序执行,先进先出。

  • Js脚本:当外部脚本 <script src="..."> 加载完成时,任务就是执行它。
  • 事件回调:例如当用户移动鼠标时,任务就是派生出 mousemove 事件和执行处理程序。
  • 定时器:当安排的(scheduled)setTimeout 时间到达时,任务就是执行其回调。

宏任务执行的间隙,如果有微任务,则浏览器先执行微任务,然后执行DOM渲染。在一个宏任务的执行过程中不进行DOM渲染,完成后才进行。

17.15.2 微任务

微任务仅来自于我们的代码。它们通常是由 promise 创建的:对 .then/catch/finally 处理程序的执行会成为微任务。微任务也被用于 await 的“幕后”,因为它是 promise 处理的另一种形式。

还有一个特殊的函数 queueMicrotask(func),它手动添加func到微任务队列,以在下次执行时机执行。

每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作。

Keywords: JavaScript
previousPost nextPost
已经有 1000000 个小伙伴看完了这篇推文。