Жывыя колекціі DOM, проблемы з распаўненням лейауту і адказ на пытанне пра прызнакі
Дазвольце дазнаць, чаму DOM ўособліва дрэваўая структура, якая рэндаруецца ў рэальны час, як гэта спрычынае прахід за межы элементаў циклу і прымусовую нарадку, а таксама чаму для спецыяльных атрыбутаў дадзеных трэба викорыстоваць метод getAttribute.
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 з’яўляюцца у звязку з процесамі лейауту, пэйнтавання і композыцыі браузера для стварэння пікселей.