Poznajemy kolejne drzewo obok DOM, czyli CSSOM. Kolejna z bardziej zaawansowanych lekcji, wymaga od nas dobrej znajomości poprzedniego materiału.

Ok, wiemy co to DOM. W tym samym momencie, w którym tworzony jest DOM, tworzy się też CSSOM. To struktura drzewa i pewne elementy są dziedziczone po innych.

Naprawmy nasz „głupi” przykład:

<body>
    <div style="width: 400px; height: 400px;">
        <p>bla bla bla in div</p>
        <div style="width: 200px; height: 200px; background-color: teal;">
            <p>bla bla bla in nested div</p>
        </div>
    </div>
    <p>Bla bla bla in body</p>
</body>

No to jest DOM, nie licząc jednego, specyficznego attribute node, jakim jest style do inline styles. To pracuje z CSSOM. Co jeszcze z nim pracuje?

Tag style, który również sobie dodamy (w sekcji head):

<style>
        
        body{
        background-color: blueviolet;
        }

        body > p {
        background-color: blueviolet;
        }

        div {
            background-color: tomato;
           }
           
        div {
        background-color: gold;
        }

        
    </style>

Czy to wszystko, co pracuje z CSSOM? Nie do końca. Przypomnijmy sobie taki kodzik:

* {
	box-sizing: border-box;
	margin: 0;
	padding: 0;
}

Co on robi? Nadpisuje domyślne style CSS. Domyślne, tzw. user-agent styles. Czyli domyślne style przeglądarki.

I tak, DOM + CSSOM tworzą nam render tree. Ważna uwaga:

  • to, co widzimy w Chrome Developer tools, to jest render tree. Widzimy tak elementy, które na stronie nie są (np. display none) oraz elementy inline styles
  • to, co widzimy w wyświetl HTML to też jest markup HTML
  • CSSOM nie jest nigdzie widoczny, nie możemy go sobie podejrzeć
  • Jednak DOM + CSSOM tworzy razem render tree, które jest renderowane w przeglądarce jako strona WWW, którą możemy podziwiać

Ok, co to znaczy, że DOM jest strukturą drzewa? Jak klasy bazowe i podklasy. Cóż, rzućmy okiem na ten fragment:

div {
   background-color: tomato;
    }
           
        div {
        background-color: gold;
        }

Wydaje się proste, div będzie mieć kolor gold, bo kolejność. Kolejność, w jakiej wrzucamy ma znaczenie. Ok, wywalimy ten tomato, ale gold przeniesiemy wyżej:

 <style>
        
        div {
            background-color: gold;
            }
        
        body{
        background-color: blueviolet;
        }

        body > p {
        background-color: blueviolet;
        }

    </style>

No i czemu body nie nadpisało background color diva? Przecież div znajduje się w body! Cóż, div nie ma background-color inherit, tylko gold.

Body jest wyżej w drzewie. Ma swoje background-color, ale już div ma override na ten background color zrobiony.

Oznacza to, że body > p też jest napisane niepotrzebnie. No chyba, że chcemy tam inny kolor ustawić, albo zmienić selektor z bezpośredniego dziecka na każdego potomka, wtedy tak, będzie zmiana.

DOM + CSSOM tworzą render tree, które rendering engine renderuje i pokazuje w przeglądarce. DOM i CSSOM są też poniekąd częścią documentu.

Podejrzyjmy sobie style CSS takim kodem:

document.styleSheets

Zobaczymy te stylesheety domyślne dla naszej przeglądarki plus inne, ze strony WWW. Dokumentacja MDN podaje nawet taką funkcję:

function getStyleSheet(unique_title) {
  for (const sheet of document.styleSheets) {
    if (sheet.title === unique_title) {
      return sheet;
    }
  }
}

Stylesheet o indeksie 1 jest, przynajmniej w Chrome, jednym z defaultowych stylów dotyczących tekstu. Wejdźmy sobie na jakąś stronę, która nie używa CSSa – może być albo taka z lat 90, albo nasza własna, niech nawet będzie to nowa karta, tam też jakiś tekst znajdziemy.

I wrzućmy taki skrypt, i patrzmy, że coś się zmieniło:

document.styleSheets[1].disabled = true;

Tak można wyłączyć stylesheet. Włączyć to false, samo disabled pokazuje, czy jest włączony czy nie…

Ok, warto to sobie uświadomić – DOM to są węzły utworzone w drzewo (XML to też drzewo DOM, tylko HTMLElementów nie ma) zaś CSSOM, to drzewo, którego nijak nam podejrzeć, ale możemy je zmieniać w różny sposób.

Czyli tak – wiemy, że elementy HTMLowe możemy zmieniać w różny sposób. Możemy im dodać do classlist klasę, która ma tam coś w CSS i w ten sposób zmienić ich wygląd.

Możemy zmieniać textContent, innerHTML, dodawać, usuwać, zmieniać atrybuty. Wreszcie, możemy poprzez atrybut style dodać coś do ich stylu. Np. za pomocą JS ustawić font-size na ileś px.

Ok, to dodawanie, a jak z odczytem? Czy mogę sobie odczytać, ile element ma px czcionki i dodać tam 2px?

Cóż, odczyt jest możliwy tylko dla inline styles. Bo inline styles to atrybut style, a atrybut jest węzłem HTML a zatem częścią drzewa DOM. Jest też brany pod uwagę przy renderowaniu, ale generalnie, CSSOM powstaje w oparciu o browser defualt stylesheets, nasze arkusze, nasze tagi style, ma konstrukcję drzewa, nie jest widoczny, nie ma go w DOMie.

To jak podejrzeć jaki CSS ma element? Cóż, DOM+CSSOM tworzą render tree, rendering engine w kilku krokach m. in ustawia layout i jednostki wielkości, kolory, i renderuje.

I w window mamy taką funkcję, która pozwala pobrać style CSS dla danego elementu, nawet jeśli nie ma on ich jako inline styles w atrybucie style:

<style>
  h3::after {
    content: " rocks!";
  }
</style>

<h3>Generated content</h3>

<script>
  const h3 = document.querySelector("h3");
  const result = getComputedStyle(h3, ":after").content;

  console.log("the generated content is: ", result); // returns ' rocks!'
</script>

Tak naprawdę to jest tam window.getComputedStyle, ale wszystko co w window jest globalnie dostępne pod swoją nazwą. I tak, można podejrzeć nawet pseudoelement after i jego atrybut content.

Generalnie tak się tego używa:

getComputedStyle(element)
getComputedStyle(element, pseudoElt)

Pseudoelement jest opcjonalny, ale i jego można podejrzeć. Dostajemy obiekt z różnymi properties.

Warto zwrócić uwagę, że requestAnimationFrame, nie tak dawno przez nas poznane to też jest obiekt window.

Teraz też widzimy, po co są document fragments oraz w przypadku frameworków frontendowych vdomy.

Z document fragment nie jest to samo, co ze StringBuilderem z jakiejś Javy, czy innego C#, gdzie ci mówią, że dodawanie do stringa przez += w pętli o milionie iteracji jest odrobinę wolniejsze niż w przypadku dodawania przez stringbuildera.

Nie, jest inaczej, tutaj dodając wiele elementów do document fragment i dopiero potem dodając fragment do dokumentu jeden raz oszczędzamy tworzenia CSSOM i renderowania przy np. każdym głupim li dodawanym do listy, które to li i tak za jednym zamachem machniemy.

Czyli jeżeli chcemy dodać kilka elementów krok po kroku to każde dodanie to jest render, zaś dodanie ich do document fragmenta to jest tylko praca na DOMie a później jeden append do czegoś na stronie to jest 1 render. Czyli profit.

Podobnie jest z VDOMem. Można przecież zrobić komponent, który za każdym ruchem robi akcję „wyrenreuj mnie”. I choć nic się nie zmieniło w tym komponencie, to coś kliknęliśmy gdzieś i już, re-render rób.

A tu frameworki frontendowe robią taki deal – tworzą VDOM, czyli wirtualny DOM. Własną implementację DOMu dla tego komponentu, to nie jest żadna specyfikacja, sam sobie ustalasz jak wygląda vDOM.

Następnie coś się zmieniło – ale frameworki frontendowe nie odświeżają strony, więc nie wiedzą co się zmieniło i czy się zmieniło. Nastąpił jakiś tick albo inna akcja, nieważne.

I wtedy nowy vDOM jest tworzony dla komponentu, stary nie jest usuwany tylko robiony jest diffing, czyli sprawdzenie, czy stary i nowy się różnią. Jak się różnią, to wtedy, stosując wszelkie możliwe optymalizacje pod spodem zapewne, framewrok frontendowy tworzy jakiś document fragment i każe zamienić starą treść komponentu na ten nowy, wywołując re-render.

Wtedy i tylko wtedy. No, tak to wygląda. Całkiem dużo już umiemy, ale wiele jeszcze zostało do odkrycia…