2022 年 4 月 30 日

修改文档

DOM 修改是创建“实时”页面的关键。

这里我们将看到如何“动态”创建新元素并修改现有页面内容。

示例:显示消息

让我们通过一个示例进行演示。我们将在页面上添加一条比 alert 更好看的提示消息。

它将如下所示

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<div class="alert">
  <strong>Hi there!</strong> You've read an important message.
</div>

这是 HTML 示例。现在让我们用 JavaScript 创建相同的 div(假设样式已在 HTML/CSS 中)。

创建元素

要创建 DOM 节点,有两种方法

document.createElement(tag)

使用给定的标签创建一个新的元素节点

let div = document.createElement('div');
document.createTextNode(text)

使用给定的文本创建一个新的文本节点

let textNode = document.createTextNode('Here I am');

大多数情况下,我们需要创建元素节点,例如消息的 div

创建消息

创建消息 div 需要 3 个步骤

// 1. Create <div> element
let div = document.createElement('div');

// 2. Set its class to "alert"
div.className = "alert";

// 3. Fill it with the content
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

我们已经创建了元素。但现在它只在一个名为 div 的变量中,尚未在页面中。因此我们看不到它。

插入方法

要使 div 显示,我们需要将其插入到 document 中的某个位置。例如,插入到 <body> 元素中,该元素由 document.body 引用。

为此,有一个特殊的方法 appenddocument.body.append(div)

以下是完整代码

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<script>
  let div = document.createElement('div');
  div.className = "alert";
  div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

  document.body.append(div);
</script>

在这里,我们在 document.body 上调用了 append,但我们可以对任何其他元素调用 append 方法,以将另一个元素放入其中。例如,我们可以通过调用 div.append(anotherElement) 将某些内容附加到 <div>

以下是更多插入方法,它们指定了不同的插入位置

  • 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

这些方法的参数是待插入的 DOM 节点的任意列表,或文本字符串(自动成为文本节点)。

让我们看看它们的作用。

以下是如何使用这些方法向列表和列表前后的文本添加项的示例

<ol id="ol">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>

<script>
  ol.before('before'); // insert string "before" before <ol>
  ol.after('after'); // insert string "after" after <ol>

  let liFirst = document.createElement('li');
  liFirst.innerHTML = 'prepend';
  ol.prepend(liFirst); // insert liFirst at the beginning of <ol>

  let liLast = document.createElement('li');
  liLast.innerHTML = 'append';
  ol.append(liLast); // insert liLast at the end of <ol>
</script>

以下是对这些方法的作用的直观描述

因此,最终列表将是

before
<ol id="ol">
  <li>prepend</li>
  <li>0</li>
  <li>1</li>
  <li>2</li>
  <li>append</li>
</ol>
after

如上所述,这些方法可以在一次调用中插入多个节点和文本片段。

例如,此处插入了一个字符串和一个元素

<div id="div"></div>
<script>
  div.before('<p>Hello</p>', document.createElement('hr'));
</script>

请注意:文本以“文本”形式插入,而不是以“HTML”形式插入,并正确转义了诸如 <> 等字符。

因此,最终 HTML 为

&lt;p&gt;Hello&lt;/p&gt;
<hr>
<div id="div"></div>

换句话说,字符串以安全的方式插入,就像 elem.textContent 所做的那样。

因此,这些方法只能用于插入 DOM 节点或文本片段。

但是,如果我们想以“HTML”形式插入一个 HTML 字符串,并让所有标签和内容正常工作,就像 elem.innerHTML 所做的那样,该怎么办?

insertAdjacentHTML/Text/Element

为此,我们可以使用另一种非常通用的方法:elem.insertAdjacentHTML(where, html)

第一个参数是一个代码字,指定相对于 elem 的插入位置。必须是以下之一

  • "beforebegin" – 在 elem 正前方插入 html
  • "afterbegin" – 在 elem 内,从开头插入 html
  • "beforeend" – 在 elem 内,从结尾插入 html
  • "afterend" – 在 elem 正后方插入 html

第二个参数是一个 HTML 字符串,以“HTML”形式插入。

例如

<div id="div"></div>
<script>
  div.insertAdjacentHTML('beforebegin', '<p>Hello</p>');
  div.insertAdjacentHTML('afterend', '<p>Bye</p>');
</script>

…将导致

<p>Hello</p>
<div id="div"></div>
<p>Bye</p>

这就是我们如何将任意 HTML 附加到页面。

以下是插入变体的图片

我们可以轻松地注意到它与上一张图片之间的相似之处。插入点实际上是相同的,但此方法插入 HTML。

该方法有两个兄弟

  • elem.insertAdjacentText(where, text) – 语法相同,但插入的 text 字符串以“文本”形式插入,而不是 HTML,
  • elem.insertAdjacentElement(where, elem) – 语法相同,但插入一个元素。

它们主要存在于使语法“统一”。在实践中,大多数时候只使用 insertAdjacentHTML。因为对于元素和文本,我们有方法 append/prepend/before/after – 它们更短,可以插入节点/文本片段。

因此,以下是一种显示消息的替代变体

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<script>
  document.body.insertAdjacentHTML("afterbegin", `<div class="alert">
    <strong>Hi there!</strong> You've read an important message.
  </div>`);
</script>

节点删除

要删除节点,有一个方法 node.remove()

让我们让我们的消息在一秒后消失

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<script>
  let div = document.createElement('div');
  div.className = "alert";
  div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

  document.body.append(div);
  setTimeout(() => div.remove(), 1000);
</script>

请注意:如果我们想将元素移动到另一个位置,则无需将其从旧位置中删除。

所有插入方法都会自动从旧位置中删除节点。

例如,让我们交换元素

<div id="first">First</div>
<div id="second">Second</div>
<script>
  // no need to call remove
  second.after(first); // take #second and after it insert #first
</script>

克隆节点:cloneNode

如何插入一条类似的消息?

我们可以创建一个函数并把代码放在那里。但另一种方法是克隆现有的 div 并修改其中的文本(如果需要)。

有时当我们有一个大元素时,这可能更快、更简单。

  • 调用 elem.cloneNode(true) 会创建一个元素的“深度”克隆 – 包含所有属性和子元素。如果我们调用 elem.cloneNode(false),那么克隆将不包含子元素。

复制消息的示例

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<div class="alert" id="div">
  <strong>Hi there!</strong> You've read an important message.
</div>

<script>
  let div2 = div.cloneNode(true); // clone the message
  div2.querySelector('strong').innerHTML = 'Bye there!'; // change the clone

  div.after(div2); // show the clone after the existing div
</script>

DocumentFragment

DocumentFragment 是一个特殊的 DOM 节点,用作传递节点列表的包装器。

我们可以向其中追加其他节点,但当我们将其插入某处时,则会插入其内容。

例如,下面的 getListContent 生成一个带有 <li> 项的片段,稍后插入到 <ul>

<ul id="ul"></ul>

<script>
function getListContent() {
  let fragment = new DocumentFragment();

  for(let i=1; i<=3; i++) {
    let li = document.createElement('li');
    li.append(i);
    fragment.append(li);
  }

  return fragment;
}

ul.append(getListContent()); // (*)
</script>

请注意,在最后一行 (*) 中,我们追加了 DocumentFragment,但它“融入了”,因此结果结构将是

<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>

DocumentFragment 很少被显式使用。为什么追加到一种特殊类型的节点,如果我们可以返回一个节点数组?重写的示例

<ul id="ul"></ul>

<script>
function getListContent() {
  let result = [];

  for(let i=1; i<=3; i++) {
    let li = document.createElement('li');
    li.append(i);
    result.push(li);
  }

  return result;
}

ul.append(...getListContent()); // append + "..." operator = friends!
</script>

我们主要提到 DocumentFragment,因为有一些概念基于它,例如 模板 元素,我们将在后面详细介绍。

老派插入/删除方法

老派
这些信息有助于理解旧脚本,但对于新开发来说并不需要。

还有一些“老派”的 DOM 操作方法,出于历史原因而存在。

这些方法来自非常古老的时代。如今,没有理由使用它们,因为现代方法(如 appendprependbeforeafterremovereplaceWith)更灵活。

我们在这里列出这些方法的唯一原因是,你可以在许多旧脚本中找到它们

parentElem.appendChild(node)

node 追加为 parentElem 的最后一个子元素。

以下示例在 <ol> 的末尾添加了一个新的 <li>

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>

<script>
  let newLi = document.createElement('li');
  newLi.innerHTML = 'Hello, world!';

  list.appendChild(newLi);
</script>
parentElem.insertBefore(node, nextSibling)

parentElem 中将 node 插入到 nextSibling 之前。

以下代码在第二个 <li> 之前插入了一个新的列表项

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>
<script>
  let newLi = document.createElement('li');
  newLi.innerHTML = 'Hello, world!';

  list.insertBefore(newLi, list.children[1]);
</script>

要将 newLi 作为第一个元素插入,我们可以这样做

list.insertBefore(newLi, list.firstChild);
parentElem.replaceChild(node, oldChild)

node 替换 parentElem 的子元素中的 oldChild

parentElem.removeChild(node)

parentElem 中删除 node(假设 node 是其子元素)。

以下示例从 <ol> 中删除第一个 <li>

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>

<script>
  let li = list.firstElementChild;
  list.removeChild(li);
</script>

所有这些方法都返回已插入/已删除的节点。换句话说,parentElem.appendChild(node) 返回 node。但通常不使用返回值,我们只运行该方法。

关于“document.write”的一句话

还有一种更古老的方法可以将某些内容添加到网页中:document.write

语法

<p>Somewhere in the page...</p>
<script>
  document.write('<b>Hello from JS</b>');
</script>
<p>The end</p>

调用 document.write(html) 会将 html 写入页面“此处此刻”。html 字符串可以动态生成,因此非常灵活。我们可以使用 JavaScript 创建一个完整的网页并将其写入。

该方法来自没有 DOM、没有标准的时代……非常久远。它仍然存在,因为有脚本在使用它。

在现代脚本中,我们很少看到它,因为有以下重要的限制

调用 document.write 仅在页面加载时有效。

如果我们随后调用它,则现有文档内容将被擦除。

例如

<p>After one second the contents of this page will be replaced...</p>
<script>
  // document.write after 1 second
  // that's after the page loaded, so it erases the existing content
  setTimeout(() => document.write('<b>...By this.</b>'), 1000);
</script>

因此,与我们上面介绍的其他 DOM 方法不同,它在“加载后”阶段有点不可用。

这是缺点。

也有优点。从技术上讲,当浏览器正在读取(“解析”)传入的 HTML 时调用 document.write,并且它写入了一些内容,浏览器会像最初在 HTML 文本中一样使用它。

因此,它的工作速度非常快,因为不涉及DOM 修改。它直接写入页面文本,而 DOM 尚未构建。

因此,如果我们需要动态地向 HTML 中添加大量文本,并且我们处于页面加载阶段,并且速度很重要,它可能会有所帮助。但在实践中,这些要求很少同时出现。而且通常我们只能在脚本中看到此方法,仅仅是因为它们很旧。

总结

  • 创建新节点的方法

    • document.createElement(tag) – 创建具有给定标记的元素,
    • document.createTextNode(value) – 创建文本节点(很少使用),
    • elem.cloneNode(deep) – 克隆元素,如果 deep==true,则克隆所有后代。
  • 插入和删除

    • 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
    • node.remove() –- 删除 node

    文本字符串以“文本”形式插入。

  • 还有一些“老派”方法

    • parent.appendChild(node)
    • parent.insertBefore(node, nextSibling)
    • parent.removeChild(node)
    • parent.replaceChild(newElem, node)

    所有这些方法都返回node

  • 给定html中的某些 HTML,elem.insertAdjacentHTML(where, html)会根据where的值插入它

    • "beforebegin" – 在elem之前插入html
    • "afterbegin" – 在 elem 内,从开头插入 html
    • "beforeend" – 在 elem 内,从结尾插入 html
    • "afterend" – 在elem之后插入html

    还有类似的方法,elem.insertAdjacentTextelem.insertAdjacentElement,它们插入文本字符串和元素,但它们很少使用。

  • 要在页面加载完成之前向页面追加 HTML

    • document.write(html)

    页面加载后,此类调用会擦除文档。主要出现在旧脚本中。

任务

重要性:5

我们有一个空 DOM 元素elem和一个字符串text

以下哪 3 个命令将执行完全相同的操作?

  1. elem.append(document.createTextNode(text))
  2. elem.innerHTML = text
  3. elem.textContent = text

答案:1 和 3

这两个命令都会将text“作为文本”添加到elem中。

这是一个示例

<div id="elem1"></div>
<div id="elem2"></div>
<div id="elem3"></div>
<script>
  let text = '<b>text</b>';

  elem1.append(document.createTextNode(text));
  elem2.innerHTML = text;
  elem3.textContent = text;
</script>
重要性:5

创建一个函数clear(elem),从元素中删除所有内容。

<ol id="elem">
  <li>Hello</li>
  <li>World</li>
</ol>

<script>
  function clear(elem) { /* your code */ }

  clear(elem); // clears the list
</script>

首先,让我们看看如何这样做

function clear(elem) {
  for (let i=0; i < elem.childNodes.length; i++) {
      elem.childNodes[i].remove();
  }
}

这不起作用,因为对remove()的调用会移动集合elem.childNodes,因此元素每次都从索引0开始。但i会增加,并且一些元素会被跳过。

for..of循环也会执行相同操作。

正确的变体可能是

function clear(elem) {
  while (elem.firstChild) {
    elem.firstChild.remove();
  }
}

还有一种更简单的方法可以执行相同操作

function clear(elem) {
  elem.innerHTML = '';
}
重要性:1

在下面的示例中,调用table.remove()会从文档中删除表格。

但如果你运行它,你可以看到文本"aaa"仍然可见。

为什么会发生这种情况?

<table id="table">
  aaa
  <tr>
    <td>Test</td>
  </tr>
</table>

<script>
  alert(table); // the table, as it should be

  table.remove();
  // why there's still "aaa" in the document?
</script>

任务中的 HTML 不正确。这就是奇怪事情的原因。

浏览器必须自动修复它。但 <table> 内可能没有文本:根据规范,只允许特定于表格的标签。因此,浏览器在 <table> 之前显示 "aaa"

现在很明显,当我们移除表格时,它仍然存在。

可以通过使用浏览器工具探索 DOM 来轻松回答这个问题。您将在 <table> 之前看到 "aaa"

HTML 标准详细规定了如何处理错误的 HTML,并且浏览器的这种行为是正确的。

重要性:4

编写一个界面,根据用户输入创建列表。

对于每个列表项

  1. 使用 prompt 询问用户其内容。
  2. 使用它创建 <li> 并将其添加到 <ul>
  3. 一直继续,直到用户取消输入(通过按 Esc 或通过空条目)。

所有元素都应动态创建。

如果用户键入 HTML 标签,则应将其视为文本。

在新窗口中演示

请注意 textContent 的用法,以分配 <li> 内容。

在沙盒中打开解决方案。

重要性:5

编写一个函数 createTree,从嵌套对象创建嵌套 ul/li 列表。

例如

let data = {
  "Fish": {
    "trout": {},
    "salmon": {}
  },

  "Tree": {
    "Huge": {
      "sequoia": {},
      "oak": {}
    },
    "Flowering": {
      "apple tree": {},
      "magnolia": {}
    }
  }
};

语法

let container = document.getElementById('container');
createTree(container, data); // creates the tree in the container

结果(树)应如下所示

选择两种解决此任务的方法之一

  1. 创建树的 HTML,然后分配给 container.innerHTML
  2. 创建树节点并使用 DOM 方法追加。

如果您能同时完成这两项工作,那就太好了。

附注:树不应该有“多余”元素,例如叶子的空 <ul></ul>

为任务打开一个沙盒。

遍历对象的最快捷方式是使用递归。

  1. 使用 innerHTML 的解决方案.
  2. 使用 DOM 的解决方案.
重要性:5

有一个组织成嵌套 ul/li 的树。

编写代码,为每个 <li> 添加其后代的数量。跳过叶子(没有子节点的节点)。

结果

为任务打开一个沙盒。

要向每个 <li> 追加文本,我们可以更改文本节点 data

在沙盒中打开解决方案。

重要性:4

编写一个函数 createCalendar(elem, year, month)

调用应为给定的年份/月份创建一个日历并将其放入 elem 中。

日历应该是一个表格,其中一周是 <tr>,一天是 <td>。表格顶部应该是 <th>,其中包含星期名称:第一天应该是星期一,依次类推到星期日。

例如,createCalendar(cal, 2012, 9) 应该在元素 cal 中生成以下日历

P.S.对于此任务,生成日历就足够了,还不需要可点击。

为任务打开一个沙盒。

我们将把表格创建为字符串:"<table>...</table>",然后将其分配给 innerHTML

算法

  1. 使用 <th> 和星期名称创建表头。
  2. 创建日期对象 d = new Date(year, month-1)。这是 month 的第一天(考虑到 JavaScript 中的月份从 0 开始,而不是 1)。
  3. 直到该月的第一天 d.getDay() 的前几个单元格可能为空。我们用 <td></td> 填充它们。
  4. 增加 d 中的天数:d.setDate(d.getDate()+1)。如果 d.getMonth() 还没有到下个月,则将新单元格 <td> 添加到日历中。如果那是星期日,则添加新行 “</tr><tr>”
  5. 如果该月已经结束,但表格行尚未填满,则向其中添加空的 <td>,以使其成为正方形。

在沙盒中打开解决方案。

重要性:4

在此处创建彩色时钟

使用 HTML/CSS 进行样式设置,JavaScript 仅更新元素中的时间。

为任务打开一个沙盒。

首先,让我们制作 HTML/CSS。

时间的每个组件在其自己的 <span> 中看起来会很棒

<div id="clock">
  <span class="hour">hh</span>:<span class="min">mm</span>:<span class="sec">ss</span>
</div>

我们还需要 CSS 来为它们着色。

update 函数将刷新时钟,由 setInterval 每秒调用一次

function update() {
  let clock = document.getElementById('clock');
  let date = new Date(); // (*)
  let hours = date.getHours();
  if (hours < 10) hours = '0' + hours;
  clock.children[0].innerHTML = hours;

  let minutes = date.getMinutes();
  if (minutes < 10) minutes = '0' + minutes;
  clock.children[1].innerHTML = minutes;

  let seconds = date.getSeconds();
  if (seconds < 10) seconds = '0' + seconds;
  clock.children[2].innerHTML = seconds;
}

在行 (*) 中,我们每次都会检查当前日期。对 setInterval 的调用不可靠:它们可能会延迟发生。

时钟管理函数

let timerId;

function clockStart() { // run the clock
  if (!timerId) { // only set a new interval if the clock is not running
    timerId = setInterval(update, 1000);
  }
  update(); // (*)
}

function clockStop() {
  clearInterval(timerId);
  timerId = null; // (**)
}

请注意,对 update() 的调用不仅在 clockStart() 中被安排,还在 (*) 行中立即运行。否则,访问者将不得不等到 setInterval 的第一次执行。并且在此之前时钟将是空的。

同样重要的是,仅当时钟未运行时才在 clockStart() 中设置一个新间隔。否则,多次单击开始按钮将设置多个并发间隔。更糟糕的是——我们只会保留最后一个间隔的 timerID,而丢失对所有其他间隔的引用。那么我们永远无法再次停止时钟了!请注意,当时钟在 (**) 行中停止时,我们需要清除 timerID,以便可以通过运行 clockStart() 再次启动它。

在沙盒中打开解决方案。

重要性:5

在此处编写代码以在两个 <li> 之间插入 <li>2</li><li>3</li>

<ul id="ul">
  <li id="one">1</li>
  <li id="two">4</li>
</ul>

当我们需要在某处插入一段 HTML 时,insertAdjacentHTML 最为合适。

解决方案

one.insertAdjacentHTML('afterend', '<li>2</li><li>3</li>');
重要性:5

有一个表格

<table>
<thead>
  <tr>
    <th>Name</th><th>Surname</th><th>Age</th>
  </tr>
</thead>
<tbody>
  <tr>
    <td>John</td><td>Smith</td><td>10</td>
  </tr>
  <tr>
    <td>Pete</td><td>Brown</td><td>15</td>
  </tr>
  <tr>
    <td>Ann</td><td>Lee</td><td>5</td>
  </tr>
  <tr>
    <td>...</td><td>...</td><td>...</td>
  </tr>
</tbody>
</table>

其中可能有多行。

编写代码以按 "name" 列对其进行排序。

为任务打开一个沙盒。

解决方案很短,但可能看起来有点棘手,因此我在此提供带有详尽注释的解决方案

let sortedRows = Array.from(table.tBodies[0].rows) // 1
  .sort((rowA, rowB) => rowA.cells[0].innerHTML.localeCompare(rowB.cells[0].innerHTML));

table.tBodies[0].append(...sortedRows); // (3)

分步算法

  1. <tbody> 中获取所有 <tr>
  2. 然后根据第一个 <td>(名称字段)的内容进行比较并对它们进行排序。
  3. 现在通过 .append(...sortedRows) 以正确的顺序插入节点。

我们不必删除行元素,只需“重新插入”,它们会自动离开旧位置。

附言:在我们的例子中,表格中有一个显式的 <tbody>,但即使 HTML 表格没有 <tbody>,DOM 结构也始终拥有它。

在沙盒中打开解决方案。

教程地图

评论

在评论之前先阅读此内容...
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行 - 将其包装在 <pre> 标记中,对于 10 行以上 - 使用沙盒 (plnkrjsbincodepen…)