Přístup k DOM není stejný jako přístup k DOM – živé vs. kolekce statických prvků

Když prohlížeč analyzuje dokument HTML, vytvoří objektový model dokumentu (DOM). Prvky HTML jsou reprezentovány jako prvky stromu DOM, ke kterým můžete přistupovat programově v JavaScriptu.

document.querySelectorAll je jednou z těchto metod přístupu DOM, ale není to jediná. Pojďme se podívat na ostatní metody a najít nějaké překvapení.

Přístup k NodeList pomocí querySelectorAll

// <html>
// <head>...</head>
// <body>
//   <ul>
//     <li>foo</li>
//     <li>bar</li>
//     <li>baz</li>
//   </ul>
// </body>
// </html>

const listItems = document.querySelectorAll('li');
console.log(listItems);        // NodeList(3) [li, li, li]
console.log(listItems.length); // 3

for (let i = 0; i < listItems.length; i++) {
  console.log(listItems[i].innerText);
}

// foo
// bar
// baz

Pokud zaznamenáte, co je vráceno document.querySelectorAll uvidíte, že máte co do činění s NodeList .

NodeLists vypadají jako pole JavaScript, ale nejsou. Pokud si přečtete NodeList Článek MDN tuto skutečnost jasně popisuje.

Překvapivě NodeLists zadejte forEach metoda. Tato metoda mi chyběla, když jsem začal pracovat ve vývoji webu, a byla to jedna z nástrah, na kterou jsem v průběhu let hodně narazil.

Navíc NodeList poskytuje další metody "podobné poli", jako je item , entries , keys a values . Přečtěte si více o těchto podrobnostech v článku MDN.

querySelectorAll je však pouze jedním způsobem přístupu k DOM. Pojďme se dozvědět více!

Kouzlo živých sbírek

Pokud si přečtete NodeList dokumentace, možná jste si všimli "malého zábavného detailu":

Ach chlapče...

Počkat, co? Živá sbírka? V některých případech?

Ukázalo se, že NodeLists se chovají odlišně v závislosti na tom, jak k nim přistupujete. Pojďme se podívat na stejný dokument a přistupovat k prvkům DOM odlišně.

// <html>
// <head>...</head>
// <body>
//   <ul>
//     <li>foo</li>
//     <li>bar</li>
//     <li>baz</li>
//   </ul>
// </body>
// </html>

// retrieve element using querySelectorAll
const listItems_querySelectorAll = document.querySelectorAll('li');
console.log(listItems_querySelectorAll); // NodeList(3) [li, li, li]

// retrieve element using childNodes
const list  = document.querySelector('ul');
const listItems_childNodes = list.childNodes;
console.log(listItems_childNodes); // NodeList(7) [text, li, text, li, text, li, text]

A NodeList přístupné přes childNodes obsahuje více prvků než NodeList vráceno document.querySelectorAll . 😲

childNodes zahrnuje textové uzly, jako jsou mezery a zalomení řádků.

console.log(listItems_childNodes[0].textContent) // "↵  "

Ale to je jen první rozdíl. Ukázalo se, že NodeLists' může být „živý“ nebo „statický“ .

Pojďme přidat další položku do seznamu dotazů a uvidíme, co se stane.

list.appendChild(document.createElement('li'));

// static NodeList accessed via querySelectorAll
console.log(listItems_querySelectorAll); // NodeList(3) [li, li, li]

// live NodeList accessed via childNodes
console.log(listItems_childNodes);       // NodeList(8) [text, li, text, li, text, li, text, li]

😲 Jak vidíte listItems_childNodes (NodeList přístupné přes childNodes ) odráží prvky modelu DOM, i když byly prvky přidány nebo odebrány. Je to "naživo".

NodeList kolekce vrácená querySelectorAll zůstává stejný. Je to reprezentace prvků, kdy byl dotazován DOM.

To už je docela matoucí, ale vydržte. Ještě jsme neskončili...

Ne každá metoda dotazování modelu DOM vrací seznam NodeList

Možná víte, že existuje více metod dotazování DOM. getElementsByClassName a getElementsByTagName vám také umožní přístup k prvkům DOM.

A ukázalo se, že tyto metody vracejí něco úplně jiného.

// <html>
// <head>...</head>
// <body>
//   <ul>
//     <li>foo</li>
//     <li>bar</li>
//     <li>baz</li>
//   </ul>
// </body>
// </html>

const listItems_getElementsByTagName = document.getElementsByTagName('li');
console.log(listItems_getElementsByTagName); // HTMLCollection(3) [li, li, li]

No dobře... HTMLCollection ?

HTMLCollection obsahuje pouze odpovídající prvky (žádné textové uzly), poskytuje pouze dvě metody (item a namedItem ) a je to živé což znamená, že bude odrážet přidané a odebrané prvky DOM.

// add a new item to the list
listItems_getElementsByTagName[0].parentNode.appendChild(document.createElement('li'));

// live HTMLCollection accessed via getElementsByTagName
console.log(listItems_getElementsByTagName); // HTMLCollection(4) [li, li, li, li]

A aby to bylo ještě složitější, HTMLCollections jsou také vráceny, když přistupujete k DOM pomocí vlastností jako document.forms nebo element.children .

// <html>
// <head>...</head>
// <body>
//   <ul>
//     <li>foo</li>
//     <li>bar</li>
//     <li>baz</li>
//   </ul>
// </body>
// </html>

const list = document.querySelector('ul');
const listItems = list.children;
console.log(listItems); // HTMLCollection [li, li, li]

Podívejte se na specifikaci HTMLCollection a najděte následující větu:

NodeList a HTMLCollection kde konkurenční standardy a nyní jsme uvízli u obou, protože nemůžeme prolomit web odstraněním funkcí.

Vývoj webu je komplikovaný

Takže shrnuto; dnes existují vlastnosti prvku DOM, jako je childNodes (vrácení živého NodeList ) a children (vrácení živého HTMLCollection ), metody jako querySelectorAll (vrácení statického NodeList ) a getElementsByTagName (vrácení živého HTMLCollection ). Přístup k DOM se nerovná přístupu k DOM!

O živých a statických sbírkách jsem ještě neslyšel, ale tento přístup k DOM mi v budoucnu ušetří spoustu času, protože najít chybu způsobenou živou sbírkou je velmi těžké.

Pokud si chcete pohrát s popsaným chováním, zkontrolujte toto CodePen.