Skip to content
This page has been auto-translated and may contain errors.View in English

O DOM

docs.scrimba.com

Seu arquivo HTML carrega uma vez, e então a página fica lá. Mas uma caixa de pesquisa que sugere resultados conforme você digita, um menu que abre ao clicar, um contador que aumenta, nada disso está no arquivo HTML. Algo muda o que aparece na página depois que ela carregou, e esse algo é JavaScript chegando até a página através do DOM. Este capítulo é sobre esse alcance: como JavaScript encontra as partes de uma página e as muda para que a página se atualize diante do leitor.

O que é o DOM

Quando o navegador carrega sua página, ele lê seu HTML e constrói uma árvore de objetos a partir dele: um objeto para a página, um para o <body>, um para cada heading, parágrafo e botão dentro. Essa árvore é o DOM, abreviação de Document Object Model. Seu JavaScript pode ler essa árvore e mudá-la, e quando faz isso, a página que você vê se atualiza imediatamente.

A parte que vale a pena guardar: o DOM não é seu arquivo HTML. É a versão viva da página, sentada na memória do navegador. Seu arquivo .html é a receita inicial; o DOM é o prato acabado que o navegador está servindo, e JavaScript pode reorganizar o prato enquanto todos ainda estão comendo.

O DOM (Document Object Model) é a árvore em memória que o navegador constrói a partir do seu HTML. Cada elemento se torna um objeto, aninhado da forma que suas tags estão aninhadas: <body> contém um <main>, que contém um <section>, que contém um parágrafo. JavaScript consegue um acesso a essa árvore através do document global, e a partir daí pode caminhar pela árvore, ler qualquer elemento e mudá-lo.

A distinção que evita confusão depois é que o DOM é separado do seu HTML fonte. O HTML é analisado uma vez para construir a árvore; depois disso, a árvore é o que está vivo. Mude o DOM e a página se atualiza; seu arquivo HTML original no disco nunca muda. É por isso que "view source" pode parecer completamente diferente do que você vê na tela uma vez que JavaScript foi executado.

O DOM é o modelo de objeto vivo do navegador do documento: uma árvore de nodes (elementos, texto, comentários e o documento em si) que o navegador constrói analisando seu HTML, e que ele renderiza. É uma API agnóstica de linguagem, exposta ao JavaScript através de document e dos objetos que penduram dele, mas não faz parte da linguagem JavaScript em si; é um recurso do navegador que o runtime fornece.

O enquadramento que importa na prática: a fonte HTML é uma entrada, analisada uma vez na árvore, e o DOM é a fonte da verdade depois disso. Leia document.body.innerHTML depois que seus scripts executam e você obtém a árvore serializada atual, não os bytes que você enviou. Isto também é por que o DOM vem direto de HTML: as tags que você escreve definem a árvore inicial, e tudo que JavaScript faz é editar essa árvore depois. Manipular o DOM tem um custo que a fonte não tem, porque as mudanças podem fazer o navegador recalcular o layout e redesenhar, que é o fio que você segue nas seções avançadas abaixo.

Cada elemento no DOM é um objeto, do mesmo tipo que você conheceu em objects, com propriedades que você pode ler e mudar. Esse é o truque inteiro: como a página é objetos, e você já sabe trabalhar com objetos, mudar a página é mudar propriedades de objeto.

JunoO que é o DOM O navegador transforma seu HTML em uma árvore de objetos chamada DOM, e essa árvore é a página viva. Seu arquivo HTML é a receita; o DOM é a refeição sendo servida, e JavaScript pode mudá-la enquanto está na mesa. Cada elemento é um objeto, então mudar a página significa mudar propriedades de objeto.
JunoO que é o DOM O DOM é a árvore em memória que o navegador constrói a partir do seu HTML, alcançada através de document. É separado do seu arquivo fonte: analisa uma vez, então a árvore é o que está vivo, é por isso que "view source" pode diferir do que você vê. Mude a árvore e a página se atualiza.
JunoO que é o DOM O DOM é uma árvore de nodes que o navegador analisa a partir de HTML e renderiza; é uma API do navegador, não parte da linguagem. A fonte é uma entrada analisada uma vez, a árvore é a verdade depois disso, então innerHTML reflete a árvore atual, não seus bytes enviados. As edições podem custar layout e redesenho, é por isso que padrões de escrita importam depois.

Selecionando elementos

Antes de poder mudar um elemento, você tem que encontrá-lo na árvore. Você aponta para ele do mesmo jeito que CSS faz, com um seletor, e o navegador entrega a você o objeto correspondente.

O workhorse é document.querySelector. Você o alimenta com um seletor CSS, o mesmo tipo de padrão que você usa em CSS, e ele retorna o primeiro elemento que corresponde:

js
const card = document.querySelector(".card"); // primeiro elemento com class="card"
const total = document.querySelector("#total"); // o elemento com id="total"

.card corresponde pela classe, #total corresponde pela id, e uma palavra simples como "button" corresponde pelo nome da tag. querySelector dá a você a primeira correspondência e para por aí. Quando você quer cada correspondência em vez disso, use querySelectorAll, que retorna uma lista que você pode percorrer:

js
const cards = document.querySelectorAll(".card"); // todos os elementos com class="card"
console.log(cards.length); // quantos encontrou

Se nada corresponder, querySelector retorna null, que é a forma do JavaScript de dizer "não há nada aqui".

document.querySelector recebe qualquer seletor CSS e retorna o primeiro elemento correspondente, ou null se nenhum corresponder. document.querySelectorAll retorna todas as correspondências como uma lista:

js
const toggle = document.querySelector(".menu-toggle"); // primeira correspondência, ou null
const items = document.querySelectorAll(".card"); // cada correspondência, como uma lista

Os seletores são CSS completo, não apenas classe e id: querySelector("nav a") encontra o primeiro link dentro de um nav, querySelector("input[type='email']") corresponde por atributo. Tudo que o seleciona em uma folha de estilos o seleciona aqui.

O retorno null é aquele para o qual você deve se preparar. Como um erro é null, código como document.querySelector(".missing").textContent lança erro, já que você não pode ler uma propriedade de null. Quando um elemento pode não existir, verifique primeiro: if (toggle) { ... }.

document.querySelector(selector) retorna o primeiro elemento correspondendo a um seletor CSS na ordem do documento, ou null; document.querySelectorAll(selector) retorna uma NodeList de todas as correspondências. Ambos aceitam a gramática completa do seletor CSS, então combinadores, seletores de atributo e pseudo-classes funcionam todos:

js
const firstError = document.querySelector(".form .field.is-invalid input");
const externalLinks = document.querySelectorAll("a[href^='http']");

Duas propriedades do resultado valem a pena conhecer. Primeiro, a NodeList de querySelectorAll é estática: é um snapshot tirado no momento da chamada, então elementos adicionados à página depois não aparecem nele. Isso é diferente das APIs mais antigas (getElementsByClassName, getElementsByTagName), que retornam uma HTMLCollection viva que se atualiza conforme o DOM muda; mais sobre essa diferença na última seção. Segundo, o escopo importa para performance e correção: chamar querySelector em um elemento em vez de document procura apenas a subárvore daquele elemento (card.querySelector(".title")), o que é tanto mais rápido em páginas grandes quanto imune a corresponder algo em outro lugar. Busque a forma com escopo sempre que você já tiver o container.

JunoSelecionando elementos Para encontrar um elemento, passe a document.querySelector um seletor CSS como .card ou #total, e ele lhe dá a primeira correspondência. Use querySelectorAll quando você quer todas como uma lista. Se nada corresponder você obtém null, que significa "nada aqui".
JunoSelecionando elementosquerySelector retorna a primeira correspondência, querySelectorAll retorna cada correspondência como uma lista, e ambas recebem qualquer seletor CSS que você usaria em uma folha de estilos. Um erro retorna null, então ler uma propriedade dele lança erro; guarde com if (element) quando o elemento pode não estar lá.
JunoSelecionando elementos Ambos os métodos recebem seletores CSS completos; querySelectorAll entrega uma NodeList estática, um snapshot que não verá adições posteriores. Chame querySelector em um elemento, não document, para procurar apenas sua subárvore, o que é mais rápido e evita corresponder algo em outra parte da página. E lembre-se que um erro é null.

Lendo e mudando elementos

Uma vez que você tem um elemento, você o lê e muda através de suas propriedades, do mesmo jeito que lê e muda qualquer objeto. É aqui que a página realmente começa a se mover.

A propriedade que você mais usará é textContent, o texto dentro de um elemento. Você pode ler ou definir:

js
const total = document.querySelector("#total");
total.textContent = "42 items"; // a página agora mostra "42 items"

Para mudar como algo se parece, você geralmente adiciona ou remove uma classe e deixa CSS fazer o estilo. classList tem três métodos para isso:

js
const card = document.querySelector(".card");
card.classList.add("selected"); // adiciona uma classe
card.classList.remove("selected"); // tira dela
card.classList.toggle("selected"); // adiciona se falta, remove se presente

Você também pode definir um estilo único diretamente através de .style, e definir um atributo como src ou href com setAttribute:

js
card.style.color = "crimson"; // um estilo inline
const avatar = document.querySelector(".avatar");
avatar.setAttribute("src", "priya.jpg"); // define a fonte da imagem

Busque classList primeiro e .style apenas para ajustes pontuais, porque manter seu estilo em CSS o mantém em um lugar.

textContent lê e escreve o texto dentro de um elemento. classList alterna classes para que o estilo permaneça em seu CSS. .style define estilos inline individuais, e setAttribute define qualquer atributo:

js
const badge = document.querySelector(".badge");
badge.textContent = `${count} new`; // define o texto
badge.classList.toggle("is-hidden", count === 0); // alterna com uma condição
badge.setAttribute("aria-label", `${count} new messages`); // define um atributo

classList.toggle recebe um segundo argumento opcional: um booleano que força a classe ativada quando true e desativada quando false, o que é mais limpo que um if/else ao redor de add e remove.

O aviso que vale a pena declarar cedo: há também innerHTML, que define o conteúdo de um elemento como HTML bruto, tags e tudo. É tentador porque parece flexível, mas use textContent, não innerHTML, para qualquer texto que veio de um usuário. Definir innerHTML a partir de entrada de usuário deixa essa entrada injetar HTML real em sua página, o que é um buraco de segurança. Quando você está definindo texto simples, textContent é a ferramenta certa de qualquer forma.

A superfície de leitura e escrita é ampla, mas quatro propriedades carregam a maioria do trabalho: textContent para texto, classList para classes de estado, .style para estilos únicos computados, e setAttribute para atributos. Prefira classList em vez de .style para que a apresentação viva em CSS e JavaScript apenas alterne classes de estado (classes como is-open ou is-loading que sua folha de estilos então estiliza):

js
const panel = document.querySelector(".panel");
panel.classList.toggle("is-open", shouldOpen);
panel.setAttribute("aria-expanded", String(shouldOpen)); // atributos são strings

A borda afiada é innerHTML. Defini-lo analisa a string como HTML e constrói nós reais a partir dela, então qualquer markup nessa string se torna vivo: um <script> ou um handler onerror em texto fornecido pelo usuário roda em sua página. Isso é cross-site scripting (XSS): injetar conteúdo executável através de dados que você tratou como texto. textContent nunca analisa HTML; define texto e apenas texto, então é o padrão seguro e o mais rápido, já que o navegador pula a análise HTML. Restrinja innerHTML para markup que você controla totalmente, e nunca o construa concatenando entrada de usuário. Quando você precisa inserir dados de usuário em markup estruturado, defina a estrutura com métodos seguros e coloque o valor não confiável através de textContent, que é o padrão que a próxima seção constrói em direção.

Uma nota mais sobre atributos: setAttribute escreve o atributo HTML (uma string), enquanto muitos elementos também expõem uma propriedade viva que pode diferir. input.value reflete o que o usuário digitou agora; input.getAttribute("value") retorna o padrão original do markup. Para campos de formulário, leia a propriedade, não o atributo.

JunoLendo e mudando elementos Defina o texto dentro de um elemento com textContent, e mude sua aparência adicionando ou removendo uma classe com classList.add, remove e toggle. Use .style para um ajuste pontual e setAttribute para coisas como uma fonte de imagem. Dependa de classes para que seu estilo permaneça em seu CSS.
JunoLendo e mudando elementostextContent para texto, classList para classes de estado, .style para pontuais, setAttribute para atributos; classList.toggle até recebe um booleano para forçá-lo ativado ou desativado. Evite innerHTML para qualquer coisa que um usuário digitou, já que injeta HTML real em sua página. Para texto simples, textContent é a ferramenta certa de qualquer forma.
JunoLendo e mudando elementos Alterne classes de estado com classList e mantenha o estilo em CSS; use setAttribute para atributos, lembrando que são strings. innerHTML analisa sua string como HTML vivo, então dados de usuário nele é um buraco XSS; textContent nunca analisa e é tanto seguro quanto rápido. E para campos de formulário leia a propriedade viva value, não o atributo.

Criando e removendo elementos

Mudar elementos que já existem o leva longe, mas às vezes não há nada lá ainda: um para-fazer que você está adicionando, um resultado de pesquisa chegando. Para isso você constrói novos elementos e os coloca na árvore, e tira os antigos.

Você faz um novo elemento com document.createElement, dá a ele algum conteúdo, então o adiciona à página com append:

js
const list = document.querySelector(".todo-list");
const item = document.createElement("li"); // um novo <li>, não na página ainda
item.textContent = "Comprar leite de aveia"; // dê a ele algum texto
list.append(item); // agora aparece, no final da lista

Um novo elemento de createElement existe apenas em memória até você append ele em algum lugar; esse é o passo que o coloca na página. Para tirar um elemento da página, chame remove nele:

js
item.remove(); // sumiu da página

document.createElement(tag) constrói um elemento, append o adiciona como o último filho de um pai, e remove tira um elemento da árvore:

js
const list = document.querySelector(".todo-list");
const item = document.createElement("li");
item.textContent = "Comprar leite de aveia"; // seguro: texto, não analisado como HTML
item.classList.add("todo-item");
list.append(item); // anexe-o à página

Defina o texto com textContent antes de anexar, não construindo uma string HTML, para que texto digitado por usuário nunca possa injetar markup. append também aceita strings simples e vários nós de uma vez (list.append(item, "ou", another)), o que é útil para conteúdo misto.

Um detalhe que pega as pessoas: criar um elemento não faz nada visível por si só. Ele fica em memória até você o anexar a algo já na árvore. Até então é real mas fora da tela, que é exatamente a propriedade que a seção avançada usa para construir eficientemente.

createElement constrói um nó desanexado, append insere nós (ou strings) como os últimos filhos de um pai, e remove desanexa um nó da árvore. A questão de eficiência é a interessante, porque inserir na árvore viva pode custar.

Cada vez que você muda o DOM de uma forma que afeta a geometria, o navegador pode precisar recalcular posições e tamanhos (reflow, também chamado de layout) e então redesenhar. Faça isso dentro de um loop, anexando um nó por iteração à página viva, e você pode disparar esse trabalho repetidamente. A correção é construir fora do DOM e inserir uma vez. Um DocumentFragment é um container leve e fora da tela para exatamente isso: você anexa nós ao fragment (que não está na árvore viva, então sem reflow), então anexa o fragment uma vez, que move seus filhos em como uma operação única:

js
const list = document.querySelector(".results");
const fragment = document.createDocumentFragment(); // container fora da tela

for (const name of names) {
  const li = document.createElement("li");
  li.textContent = name; // inserção segura de dados de usuário
  fragment.append(li); // sem reflow: fragment está fora do DOM
}

list.append(fragment); // uma inserção na árvore viva

Anexar o fragment o esvazia no pai, então o fragment em si não aparece no DOM; apenas seus filhos aparecem. O mesmo princípio impulsiona um hábito mais amplo: lotes de escritas DOM. Ler uma propriedade de layout (como offsetHeight) força o navegador a liberar layout pendente, então intercalar leituras e escritas em um loop causa layout thrash, refluxos forçados repetidos. Agrupe suas leituras, então agrupe suas escritas. Para um punhado de nós nada disso importa; para centenas é a diferença entre suave e gaguejante.

JunoCriando e removendo elementos Construa um novo elemento com document.createElement, dê a ele texto com textContent, e coloque-o na página com append. Um elemento fresco vive apenas em memória até você anexá-lo, então esse passo é o que o faz aparecer. Para tirar um da página, chame remove nele.
JunoCriando e removendo elementoscreateElement constrói, append anexa como o último filho, remove tira. Defina texto com textContent antes de anexar em vez de construir uma string HTML, para que nada que um usuário digitou possa injetar markup. Lembre-se que um elemento criado permanece invisível até você anexá-lo a algo já na página.
JunoCriando e removendo elementos Anexar à árvore viva em um loop pode disparar reflow cada vez; construa em um DocumentFragment fora do DOM e anexe uma vez para que os filhos se movam juntos. Ler uma propriedade de layout no meio do loop força layout e causa thrash, então lote suas leituras e depois suas escritas. Para alguns nós é discutível; para centenas é suave versus gaguejante.

Para onde o DOM vai daqui

Selecionar, ler, mudar, criar e remover cobrem a maioria do que JavaScript faz em uma página, e tudo isso é o mesmo movimento por baixo: encontrar um objeto na árvore e mudar suas propriedades. Porque o DOM é objetos, tudo que você aprendeu em objects se aplica diretamente aqui, e o pequeno conjunto de métodos neste capítulo (querySelector, textContent, classList, createElement, append) é a maioria do kit de ferramentas.

O que falta até agora é timing. Cada exemplo aqui roda uma vez, de cima para baixo, mas uma página real muda em resposta ao que o leitor faz: um clique, uma digitação, um formulário enviado. Conectar suas mudanças de DOM a esses momentos é events, o próximo capítulo, onde addEventListener conecta "o usuário fez isso" a "então a página faz aquilo". Uma vez que selecionar e mudar se sentam ao lado de responder a eventos, você pode construir as páginas interativas para as quais o resto deste handbook é direcionado.