HDP115

Manipulación del DOM Avanzada

Técnicas sofisticadas para interactuar con el DOM.

CE

Cristian Escalante

Última actualización: 23 de abril de 2025

javascript
programación web
desarrollo

Manipulación del DOM Avanzada

Técnicas sofisticadas para interactuar con el DOM:

  • Manipulación eficiente del DOM
  • Traversing avanzado
  • Virtual DOM y su concepto
  • Web Components
  • Shadow DOM
  • Templates y slots
  • IntersectionObserver y LazyLoading
  • Animaciones con JavaScript

Manipulación eficiente del DOM

La manipulación constante del DOM puede afectar negativamente al rendimiento de una aplicación web. Aquí hay algunas técnicas para optimizar la manipulación del DOM:

Minimizar la recomputación del layout

// Ineficiente: causa múltiples reflows
for (let i = 0; i < 100; i++) {
  elemento.style.width = i + 'px';
}

// Eficiente: agrupa cambios
function actualizarEstilos() {
  // Leer
  const width = elemento.offsetWidth;
  
  // Agrupar escrituras
  elemento.style.width = (width + 10) + 'px';
  elemento.style.height = (width + 10) + 'px';
  elemento.style.margin = (width / 10) + 'px';
}

Uso de fragmentos de documento

// Crear un fragmento (no forma parte del DOM)
const fragmento = document.createDocumentFragment();

// Añadir elementos al fragmento
for (let i = 0; i < 1000; i++) {
  const item = document.createElement('li');
  item.textContent = `Item ${i}`;
  fragmento.appendChild(item);
}

// Insertar el fragmento en el DOM (una sola operación)
document.getElementById('lista').appendChild(fragmento);

Modificación por lotes

// Ocultar elemento durante modificaciones
const elemento = document.getElementById('contenedor');
const displayOriginal = elemento.style.display;
elemento.style.display = 'none';

// Realizar múltiples modificaciones
realizarCambiosComplejos(elemento);

// Mostrar elemento nuevamente
elemento.style.display = displayOriginal;

Traversing avanzado del DOM

El traversing del DOM se refiere a la navegación a través de los nodos del árbol del documento.

Relaciones entre nodos

const elemento = document.querySelector('.item');

// Navegación vertical
const padre = elemento.parentNode; // o parentElement
const hijos = elemento.children; // solo elementos (no texto ni comentarios)
const todosLosHijos = elemento.childNodes; // todos los tipos de nodos
const primerHijo = elemento.firstChild; // primer nodo hijo
const primerElementoHijo = elemento.firstElementChild; // primer elemento hijo
const ultimoHijo = elemento.lastChild; // último nodo hijo
const ultimoElementoHijo = elemento.lastElementChild; // último elemento hijo

// Navegación horizontal
const siguienteHermano = elemento.nextSibling; // siguiente nodo hermano
const siguienteElementoHermano = elemento.nextElementSibling; // siguiente elemento hermano
const anteriorHermano = elemento.previousSibling; // anterior nodo hermano
const anteriorElementoHermano = elemento.previousElementSibling; // anterior elemento hermano

Búsqueda avanzada de elementos

// Buscar el ancestro más cercano que coincida con un selector
const ancestro = elemento.closest('.contenedor');

// Comprobar si un elemento contiene a otro
const contiene = padre.contains(hijo);

// Obtener la posición de un elemento entre sus hermanos
const indice = Array.from(elemento.parentNode.children).indexOf(elemento);

Virtual DOM y su concepto

El Virtual DOM es una representación ligera en memoria del DOM real utilizada por frameworks como React para optimizar las actualizaciones de la interfaz de usuario.

Cómo funciona el Virtual DOM

  1. Creación: Se crea una representación virtual del DOM en memoria.
  2. Manipulación: Las operaciones se realizan sobre esta copia virtual.
  3. Reconciliación: Se compara el estado anterior con el nuevo.
  4. Actualización: Solo se aplican al DOM real los cambios necesarios.
// Implementación simplificada de un Virtual DOM
class VirtualNode {
  constructor(tagName, props = {}, children = []) {
    this.tagName = tagName;
    this.props = props;
    this.children = children;
  }
  
  render() {
    // Crear el elemento DOM real
    const element = document.createElement(this.tagName);
    
    // Aplicar propiedades
    Object.entries(this.props).forEach(([key, value]) => {
      if (key === 'style' && typeof value === 'object') {
        Object.assign(element.style, value);
      } else if (key.startsWith('on') && typeof value === 'function') {
        element.addEventListener(key.substring(2).toLowerCase(), value);
      } else {
        element.setAttribute(key, value);
      }
    });
    
    // Renderizar hijos
    this.children.forEach(child => {
      if (typeof child === 'string') {
        element.appendChild(document.createTextNode(child));
      } else {
        element.appendChild(child.render());
      }
    });
    
    return element;
  }
}

// Ejemplo de uso
const vNode = new VirtualNode('div', { class: 'container' }, [
  new VirtualNode('h1', {}, ['Título']),
  new VirtualNode('p', { style: { color: 'blue' } }, ['Contenido'])
]);

// Renderizar en el DOM
document.body.appendChild(vNode.render());

Web Components

Los Web Components son un conjunto de tecnologías que permiten crear elementos HTML personalizados reutilizables.

Componentes principales

  1. Custom Elements: API para definir elementos HTML personalizados
  2. Shadow DOM: Encapsulación de estilos y estructura
  3. HTML Templates: Fragmentos HTML declarativos
  4. HTML Imports: Incluir y reutilizar HTML (obsoleto)

Creación de un Custom Element

// Definir la clase del componente
class MiComponente extends HTMLElement {
  constructor() {
    super();
    
    // Crear Shadow DOM
    this.attachShadow({ mode: 'open' });
    
    // Definir estructura interna
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          border: 1px solid #ccc;
          padding: 16px;
        }
        .titulo {
          color: blue;
        }
      </style>
      <div>
        <h2 class="titulo">${this.getAttribute('titulo') || 'Título predeterminado'}</h2>
        <slot></slot>
      </div>
    `;
  }
  
  // Ciclo de vida: cuando el elemento se añade al DOM
  connectedCallback() {
    console.log('Componente añadido al DOM');
  }
  
  // Ciclo de vida: cuando el elemento se elimina del DOM
  disconnectedCallback() {
    console.log('Componente eliminado del DOM');
  }
  
  // Ciclo de vida: cuando cambia un atributo observado
  attributeChangedCallback(nombre, valorAntiguo, valorNuevo) {
    if (nombre === 'titulo' && this.shadowRoot) {
      this.shadowRoot.querySelector('.titulo').textContent = valorNuevo;
    }
  }
  
  // Definir qué atributos observar
  static get observedAttributes() {
    return ['titulo'];
  }
}

// Registrar el componente
customElements.define('mi-componente', MiComponente);

Uso del componente personalizado

<mi-componente titulo="Mi título personalizado">
  <p>Este contenido se insertará en el slot</p>
</mi-componente>

Shadow DOM

El Shadow DOM permite encapsular el marcado, el estilo y el comportamiento de un componente, aislándolo del resto del documento.

Creación y uso del Shadow DOM

// Crear un elemento host
const host = document.createElement('div');
document.body.appendChild(host);

// Crear y adjuntar un Shadow DOM
const shadowRoot = host.attachShadow({ mode: 'open' });
// mode: 'open' permite acceder desde JavaScript externo
// mode: 'closed' oculta el shadowRoot de JavaScript externo

// Añadir contenido al Shadow DOM
shadowRoot.innerHTML = `
  <style>
    /* Estos estilos solo afectan al interior del Shadow DOM */
    p {
      color: red;
      font-weight: bold;
    }
  </style>
  <p>Este texto está dentro del Shadow DOM</p>
`;

// Los estilos del documento principal no afectan al contenido del Shadow DOM
document.head.innerHTML += `
  <style>
    p {
      color: blue;
      font-style: italic;
    }
  </style>
`;

Encapsulación de estilos

// Selectores especiales en Shadow DOM
shadowRoot.innerHTML += `
  <style>
    /* Estilo para el elemento host */
    :host {
      display: block;
      border: 1px solid black;
    }
    
    /* Estilo para el host cuando tiene una clase */
    :host(.destacado) {
      background-color: yellow;
    }
    
    /* Estilo para el host basado en el contexto */
    :host-context(.tema-oscuro) {
      background-color: #333;
      color: white;
    }
  </style>
`;

Templates y slots

Las plantillas HTML y los slots permiten definir fragmentos de HTML reutilizables con espacios reservados para contenido personalizado.

Uso de templates

<!-- Definición de una plantilla -->
<template id="mi-template">
  <style>
    .contenedor {
      border: 1px solid #ccc;
      padding: 10px;
    }
    .titulo {
      color: navy;
    }
  </style>
  <div class="contenedor">
    <h2 class="titulo">Título de la plantilla</h2>
    <slot name="contenido">Contenido predeterminado</slot>
    <footer>
      <slot name="pie">Pie predeterminado</slot>
    </footer>
  </div>
</template>
// Uso de la plantilla en JavaScript
class ComponenteConTemplate extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    
    // Clonar el contenido de la plantilla
    const template = document.getElementById('mi-template');
    const contenido = template.content.cloneNode(true);
    
    // Añadir al Shadow DOM
    this.shadowRoot.appendChild(contenido);
  }
}

customElements.define('componente-template', ComponenteConTemplate);

Uso de slots

<!-- Uso del componente con slots -->
<componente-template>
  <div slot="contenido">
    <p>Este es mi contenido personalizado</p>
  </div>
  <p slot="pie">Copyright 2023</p>
</componente-template>

IntersectionObserver y LazyLoading

El IntersectionObserver API permite detectar cuándo un elemento entra o sale del viewport del navegador, lo que facilita implementar técnicas como lazy loading.

Implementación básica de IntersectionObserver

// Crear un observador
const observador = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    // entry.isIntersecting será true cuando el elemento sea visible
    if (entry.isIntersecting) {
      console.log('El elemento es visible en el viewport');
      
      // Opcional: dejar de observar el elemento
      observer.unobserve(entry.target);
    }
  });
}, {
  // Opciones
  root: null, // viewport
  rootMargin: '0px', // margen alrededor del root
  threshold: 0.1 // porcentaje de visibilidad necesario (10%)
});

// Observar un elemento
const elemento = document.querySelector('.mi-elemento');
observador.observe(elemento);

Implementación de lazy loading de imágenes

// Función para cargar imágenes cuando sean visibles
function lazyLoadImages() {
  const imagenes = document.querySelectorAll('img[data-src]');
  
  const observador = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        
        // Cargar la imagen real
        img.src = img.dataset.src;
        
        // Eliminar el atributo data-src
        img.removeAttribute('data-src');
        
        // Dejar de observar la imagen
        observer.unobserve(img);
      }
    });
  });
  
  // Observar todas las imágenes
  imagenes.forEach(img => observador.observe(img));
}

// Iniciar lazy loading
document.addEventListener('DOMContentLoaded', lazyLoadImages);

HTML con imágenes para lazy loading

<img src="placeholder.jpg" data-src="imagen-real-1.jpg" alt="Descripción">
<img src="placeholder.jpg" data-src="imagen-real-2.jpg" alt="Descripción">
<img src="placeholder.jpg" data-src="imagen-real-3.jpg" alt="Descripción">

Animaciones con JavaScript

JavaScript permite crear animaciones manipulando propiedades de elementos a lo largo del tiempo.

Animación con requestAnimationFrame

function animar(elemento, propiedades, duracion) {
  const inicio = performance.now();
  const valoresIniciales = {};
  const cambios = {};
  
  // Calcular valores iniciales y cambios
  for (const prop in propiedades) {
    valoresIniciales[prop] = parseFloat(getComputedStyle(elemento)[prop]) || 0;
    cambios[prop] = propiedades[prop] - valoresIniciales[prop];
  }
  
  // Función de animación
  function paso(tiempoActual) {
    const tiempoTranscurrido = tiempoActual - inicio;
    const fraccion = Math.min(tiempoTranscurrido / duracion, 1);
    
    // Actualizar propiedades
    for (const prop in propiedades) {
      const valor = valoresIniciales[prop] + cambios[prop] * fraccion;
      elemento.style[prop] = `${valor}px`;
    }
    
    // Continuar la animación si no ha terminado
    if (fraccion < 1) {
      requestAnimationFrame(paso);
    }
  }
  
  // Iniciar animación
  requestAnimationFrame(paso);
}

// Ejemplo de uso
const caja = document.querySelector('.caja');
animar(caja, { width: 300, height: 200 }, 1000);

Funciones de easing

const funciones = {
  // Lineal
  lineal: t => t,
  
  // Ease in
  easeInQuad: t => t * t,
  easeInCubic: t => t * t * t,
  
  // Ease out
  easeOutQuad: t => t * (2 - t),
  easeOutCubic: t => (--t) * t * t + 1,
  
  // Ease in-out
  easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
};

// Animación con función de easing
function animarConEasing(elemento, propiedades, duracion, funcionEasing) {
  const inicio = performance.now();
  const valoresIniciales = {};
  const cambios = {};
  
  for (const prop in propiedades) {
    valoresIniciales[prop] = parseFloat(getComputedStyle(elemento)[prop]) || 0;
    cambios[prop] = propiedades[prop] - valoresIniciales[prop];
  }
  
  function paso(tiempoActual) {
    const tiempoTranscurrido = tiempoActual - inicio;
    const fraccion = Math.min(tiempoTranscurrido / duracion, 1);
    const fraccionConEasing = funcionEasing(fraccion);
    
    for (const prop in propiedades) {
      const valor = valoresIniciales[prop] + cambios[prop] * fraccionConEasing;
      elemento.style[prop] = `${valor}px`;
    }
    
    if (fraccion < 1) {
      requestAnimationFrame(paso);
    }
  }
  
  requestAnimationFrame(paso);
}

// Ejemplo
animarConEasing(caja, { width: 300 }, 1000, funciones.easeOutCubic);

Web Animations API

// Animación usando la API nativa
elemento.animate([
  // Fotogramas clave (keyframes)
  { transform: 'translateX(0)', opacity: 1 },
  { transform: 'translateX(100px)', opacity: 0.5, offset: 0.7 },
  { transform: 'translateX(200px)', opacity: 0 }
], {
  // Opciones
  duration: 2000, // duración en ms
  easing: 'ease-in-out', // función de temporización
  delay: 500, // retraso antes de iniciar
  iterations: 2, // número de repeticiones
  direction: 'alternate', // alternar dirección en repeticiones
  fill: 'forwards' // mantener el estado final
});

// Control de la animación
const animacion = elemento.animate(/* ... */);
animacion.pause(); // pausar
animacion.play(); // reproducir
animacion.reverse(); // invertir dirección
animacion.finish(); // ir al final
animacion.cancel(); // cancelar

// Eventos
animacion.onfinish = () => console.log('Animación finalizada');
animacion.oncancel = () => console.log('Animación cancelada');

Conceptos Básicos de HTML

Aprende los conceptos básicos de HTML

Conceptos Básicos de CSS

Aprende los conceptos básicos de CSS

Conceptos Básicos SQL

Aprende los conceptos básicos de SQL

Conceptos Básicos de GIT

Aprende los conceptos básicos de GIT

Conceptos Básicos de Python

Aprende los conceptos básicos de Python

Conceptos Básicos de UML

Aprende los conceptos básicos de UML

Refuerzo Academico de Herramientas de Productividad 2025