Галоўная / Артыкулы / Жывыя колекціі DOM, проблемы з распаўненням лейауту і адказ на пытанне пра прызнакі

Жывыя колекціі DOM, проблемы з распаўненням лейауту і адказ на пытанне пра прызнакі

Дазвольце дазнаць, чаму DOM ўособліва дрэваўая структура, якая рэндаруецца ў рэальны час, як гэта спрычынае прахід за межы элементаў циклу і прымусовую нарадку, а таксама чаму для спецыяльных атрыбутаў дадзеных трэба викорыстоваць метод getAttribute.

1407 слоў

DOM частаўкаецца медленнасцю, і звычныя адказы на гэта — што ён проста моцная структура. Аднак гэты адказ здароўа некалькі разоў неправы. Аперацыі з DOM є досыта швядкія; проблема крыўціцца ў распалоўжаным шаблоне кодавання, калі ў цикле паўтараючыся запытаецца DOM пра інформацыю, а потым прабуецца змяніць яго. Ёжы разумець чаму, спачатку трэба адносна точна зразумець, што такое DOM, і гэты ж падход дапамагае зрозумець два іншыя явішчы, якія часта плутаюць разработчыкаў: циклы, якія прахоўваюць элементы, і спецыяльныя атрыбуты, якія вяртаюць значэнне undefined.

DOM — жывое дрэва, а не копія вашага HTML

Калі браузер парсуе HTML, ён не зберагае маркап як текст. У кожны элемент ён стварае об’ект, а гэтыя об’екты накладваюцца адпаведна да маркапу, ствараючы дрэва. Разглянем маленькі каталог продуктав:

<div id="catalog">
  <div class="product">
    <h3>Desk Lamp</h3>
    <span class="price">$34</span>
  </div>
  <div class="product">
    <h3>Standing Mat</h3>
    <span class="price">$58</span>
  </div>
</div>

На адыме з гэтага прыгравер стварае дрэва жывых об’ектаў, да якіх можа дасягнуць JavaScript: #catalog мае два элементы .product, і кожны з іх мае элемент <h3> і элемент <span>. Ключовы момант — гэтае дрэва не ўсё тое, што было створанае раз і пасля таго заставленае. Цэя структура — гэта тое, што прыгравер адрасавае ў даны момант. Як толькі вы змяніце якую-небудзь власнасць адного з гэтых об’ектаў-вузлаў, стораніца гэта адбівае; няма окалічнага крока з запісам або вызову адрасавання.

document.querySelector(".product h3").textContent = "Desk Lamp (Sale)";

Гэтае запэўненне не ёсць запросам, які прыгравер будзе обрабляць пазней замест вас. Сама прызначэнне — гэта сама змяна. Усвядомленне гэтай „жывой“ прыроды є найкорыстнейшым ментальным моделюванням для DOM, і гэта таксама ёсць прычыной бага, які практычна кожны стварые ў какі-небудзь момант.

Чаму цикл над жывым калекцыяй скакае элементы

Дзейніце, вы хочаце адчыніць кожную картку товару, пазначаную як без наявнасці. Вочыма здаецца, што цикл должен выглядзець так:

const outOfStockCards = document.getElementsByClassName("out-of-stock");

for (let i = 0; i < outOfStockCards.length; i++) {
  outOfStockCards[i].remove();
}

Ён здаецца правільным, але тыха залишае кожны другі элемент. Прычына ў тым, што getElementsByClassName не вяртае статычны масів. Ён вяртае жывую HTMLCollection, якая адпрацоўвае дакумент у момент змены. Як толькі .remove() адчынае першую картку, калекцыя становіцца на адны элемент корачэй, і кожная застаўшаяся картка перасуваецца на адны індэкс назад. Лічыльнік циклу продовжае працаваць, таму элемент, які ўжо знаходзіцца на індэксе 0, так і не адвідуецца. Размер калекцыі зміняецца пад час виконання циклу, і прыблізна палова элементаў застаецца без увагі.

Рашэнне — зробіць статычную копію перш чым прынямаць змены:

const outOfStockCards = Array.from(document.getElementsByClassName("out-of-stock"));

for (const card of outOfStockCards) {
  card.remove();
}

Array.from стварае копію жывага колекцыі у звычны масэв, таму пазнейшая выдаленне элементаў не можа зменшыць аб’ект, па якому вы ведзеце ітерацыю. Простейшы варыянт — document.querySelectorAll(".out-of-stock"), які з самага пачатку вярта статычны NodeList і не выклікае ніякой пераканвертавання. Гэтая прыемная працэздатнась ёсць аднай з прычын, чаму querySelectorAll пераважна заменіў старэйшыя API для пошуку ў сучасных кодавых базах. Якщо вы прымушаны работаць з жывай колекцыяй, ітерацыя назад ад последнага індэкса таксама дапамагае ухіліцца ад змян, хоць статычная копія зазвычай є болей зрозумелай.

Проблемы з лэйаутам: справжня прычына медленнай роботы коду DOM

Тепер працэўная проблема. Вычысленне лейауту, тое самае — точныя размеры і распадзел кожнага элемента, ўскладнена. Браузеры стараюцца не робіць гэта больш, чым трэба: калі вы зменяеце стылі, яны не перывучаюць лейаут негайна. Яны збіраюць усі запланаваныя змены і вычысляюць лейаут адной час, за некалькі секундаяў да падготавкі наступнага кадру.

Гэтая стратэгія працюе толькі тады, калі ваш код гэта дазваляе. Дзеяныя власцівасці і методы, укладаючы offsetWidth, offsetHeight і getBoundingClientRect(), патрабуюць апошніх дадзенняў геаметрыі, каб вернуць значэнне. Їх чытанне змушвае браузер выконваць вычысленне лейауту сінхрональна, адтаколькі ён не можа падаць шырыню элемента без яе вычыслення.

У наступным цыкле для кожнай карткі чергуецца чытанне і запіс:

// forces a full layout recalculation on every single iteration
const cards = document.querySelectorAll(".product");
cards.forEach((card) => {
  const width = card.offsetWidth; // read: forces layout
  card.style.width = width + 10 + "px"; // write: invalidates layout again
});

Кожная ітерацыя чытае шырыню, а пасля запісвае новую. Чытанне не можа базавацца на застарэлых данных, адколькі змены стылю, запісаныя ў попераднай ітерацыі, могу паўлічыць рэзультат. Таму браузер абрабоцвае чакаючыя змены, пераскладывае лэй아ут, вяртае число, а ўжо наступны рядок зноў унэвалідавае гэты лэй아ут. Гэты цыкл вядомы як «layout thrashing» (або прымусовы сінхронны лэйаут), і самэ гэта люди насправдзе перажываюць, калі кажуць, што DOM працуе медленна. DOM выкананае дорогія задачы набагато частае, чым трэба, таму што код постаўляе такія вимаганні.

Раздзеліце работу на два этапы: спачатку всі чытанні, а пасля — всі запісвы:

const cards = document.querySelectorAll(".product");
const widths = Array.from(cards).map((card) => card.offsetWidth); // all reads, together
cards.forEach((card, i) => {
  card.style.width = widths[i] + 10 + "px"; // all writes, together
});

Тепер шырэйка вычысляецца адназнач, калі чытаецца першая значэнне шырэйкі, а наступныя чытання вжываюць той самы рэзультат, калі ў межах часу нічога не змянілася. Запісы тады складаюцца ў чергу і обрабоцваюцца разам перад наступным атрыбутам. DOM не стаў быстрэйшым; код проста перастаў змушваць яго выконваць тыя ж вычысленні ў кожной ітерацыі.

З гэтага вылучаюцца калькі практычных заўважэнняў:

  • Тая ж правіла прыменяюцца да іншых чытанняў геаметрыі, такіх як clientWidth, scrollTop і getComputedStyle().
  • У большых прыкладах програмі чытання і запісы часта выконвуцца ў разных функцыях або компанентах, таму можа выйсці проблема, нават калі жаданага циклу не вidaць. Інструменты для аналізу выконання браузера падсвечваюць прычыны змін шырэйкі, чыяму лёгкае ўсталяць іх.
  • Планаванне запісу з requestAnimationFrame — это распашчытаны спосаб прытвараць іх разам непраздле рэндэравання.
  • Атрыбуты і свайперы не завжды ўсё тое ж

    Ішчэ адна неспадзянка каменуецца заўсёды з адносам між атрыбутамі HTML і свайперамі JavaScript. Яны зазвычай адпавядаюць адна другой, але гэта адпаведанне мае меры, і гэтыя меры становяцца значнымі як толькі вы дадаеце спецыяльныя даны. Возьмім гэты элемент:

    <div class="product" data-sku="LAMP-2201"></div>
    

    І гэты код, які чытае з яго тры разныя способамі:

    const el = document.querySelector(".product");
    console.log(el.className); // "product" — standard attributes map to properties directly
    console.log(el.sku);       // undefined — custom attributes don't
    console.log(el.getAttribute("data-sku")); // "LAMP-2201" — this is how you actually reach it
    

    Стандартныя атрыбуты, вядомыя браузеру, такія як href, src і class, апоштова станавяцца відпаваемымі сваямоścю. class практычна выступае як className, таму што class быў зарэзерваваным словам у JavaScript. Спецыяльныя атрыбуты не павінны такае стаўленне, укладаючы ў сябе тыя, якія пачынаюцца з data- — стандартнага механізма для зберагчэння власных метадаў на элементах. Сваямоśць el.sku не існуе, таму выкарыстоўванне вяртае undefined, і для чытання або змены значэння патрэбны getAttribute і setAttribute. Для дадатковага зручнасці браузеры таксама апоштова выкладзяюць data- атрыбуты через об’ект dataset, таму el.dataset.sku вяртае тое ж самае значэнне. Несувязанасць незначная, але самэ гэта і стварае прамысловасць.

    ode>undefined — гэта тады, калі вы спакульватеся, што спецыяльны атрыбут будзе паводліваць сабе як href.

    Адна ментальная модель замест трох прынцыпоў

    Гэтыя паводзкі не ёсу аддзельными фактамі. Усе яны вынікаюць з адной праўды: DOM — гэта жывая структура, якая адрасаваецца, а не нерухомыя даны, якія заполняюцца толькі раз.

    • Жывыя колекцыі зменяюцца пад час ўточнення ў яных, таму зробіце іх копію або выкарыстаўце querySelectorAll прытаманне перад змінамі.
    • Чытанне геаметрыя прыводзіць да таго, што браузер негайна вычысляе ўтварэнне, таму чытайце даны пакетамі перад запісамі.
    • Атрыбуты JavaScript выступаюць як зручнасць над атрыбутамі і не відображаюць усе з яных, таму да спецыяльных дадзеных дасягайце через getAttribute або dataset.

    Калі вы будете спрыглядзець на DOM як на жывое дрэва, а не на медленную структуру дадзейнаў, гэтыя прыбліжанні больш не будуць неспадзячаннямі, а стануць прыкладнымі наследкамі. Чыба пазнаць, як у контэксте фреймворка від змены стану да пікселей адбываецца рэндарынг, пагляньце на як React ператварае змены стану на пікселі на экране.

    Спаднёючая літэратура

    • React Rendering Explained: State Updates to Screen Pixels — Дазнаецеся, як фазы рэндарынгу, супраўжнечкі і фіксацыі ў React з’яўляюцца у звязку з процесамі лейауту, пэйнтавання і композыцыі браузера для стварэння пікселей.
  • React 19.2: Адказанне пра новым элементам, хукам і методам — Дазвольце дазнацца, як новы элемент Activity, хук useEffectEvent і частковая статычная рэндарызацыя ў React 19.2 скасоўваюць захаваныя проблемы з выдаткамі ресурсаў у сучасных інтерфейсах.