La API para JavaScript del navegador
Desarrollo de Aplicaciones en Internet
Qué hacer con las APIs web
- Escuchar eventos (por ejemplo, un clic del ratón o el borrado de un elemento del DOM) y ejecutar código en respuesta a ellos.
- Modificar en vivo el HTML y el CSS de un documento.
- Intercambiar datos con un servidor (lo veremos en otro tema).
- Retardar la ejecución de un código hasta dentro de un tiempo (setTimeout).
- Almacenar información en una base de datos del navegador.
Qué hacer con las APIs web
- Interactuar con webcams, geolocalización, micrófono, etc.
- Realizar animaciones (por ejemplo, mediante canvas o SVG).
- Ejecutar algoritmos.
- …
El árbol DOM
- Como ya vimos, los nodos representan elementos, contenido textual, comentarios, etc.
- Jerarquía de tipos para los elementos: Node <- Element <- {HTMLElement, SVGElement}
Objetos window y document
- El objeto global window (tipo Window) representa la ventana que contiene un documento DOM; podemos acceder a numerosas propiedades como window.innerWidth.
- El objeto global document (tipo Document) es la puerta de entrada al árbol DOM; es el objeto que vamos a usar más a menudo, por ejemplo, a través de funciones como querySelector.
- El objeto document.body permite acceder al cuerpo del documento.
Acceso con selectores CSS
- querySelector devuelve un elemento (tipo Element); se devuelve el primero si más de uno satisface el criterio del selector
- querySelectorAll devuelve una lista no viva (es decir, la lista no se actualiza dinámicamente) de elementos
var el = document.querySelector(".miClase");
var images = document.querySelectorAll("img.pictures, img.charts");
for (var i = 0; i < images.length; i++) {
console.log(images[i].getAttribute("src"));
}
Antes de querySelector
- Estos dos fragmentos de código son equivalentes, pero la primera forma (estandarizada más recientemente) es mucho más compacta:
document.querySelector("#settingsForm > table > tbody > tr:nth-child(2) > td:first-child")
.innerHTML+= "<p>Tim Berners-Lee</p>";
document.getElementById("settingsForm").getElementsByTagName("table")[0]
.getElementsByTagName("tbody")[0].getElementsByTagName("tr")[1]
.getElementsByTagName("td")[0].innerHTML+= "<p>Tim Berners-Lee</p>";
Consulta y modificación del DOM
- Modificación del contenido textual de un elemento:
var title = document.querySelector("#intro");
title.textContent = "Ulises";
Consulta y modificación de atributos
var title = document.querySelector("#picture");
title.setAttribute("src", "http://www.example.com");
var t = title.getAttribute("alt");
var i = title.id; // para id y class no es necesario usar los métodos anteriores
title.className = "bar foo"; // el id y la clase se exponen como atributos de un elemento
Consulta y modificación de clases
- Una manera mejor que className de gestionar las clases de un elemento.
var divElement = document.querySelector("#myDiv");
divElement.classList.add("bar");
divElement.classList.remove("foo");
divElement.classList.toggle("foo");
if (divElement.classList.contains("bar")) {
....
}
divElement.className = ""; // borra todas las clases
Atributo style de un elemento
<!doctype html>
<html><head><title>simple style example</title>
<script type="text/javascript">
function alterStyle(elem) {
elem.style.backgroundColor = "green"; // los guiones se eliminan en el nombre del atributo
}
function resetStyle(elemId) {
elem = document.getElementById(elemId);
elem.style.backgroundColor = 'white';
}
</script>
</head>
<body>
<p id="p1" onclick="alterStyle(this);">Click here to change background color.</p>
<button onclick="resetStyle('p1');">Reset background color</button>
<!-- Es muy recomendable añadir los eventos con addEventListener y no con onclick -->
</body>
</html>
Consulta y modificación de estilos
- style solo devuelve propiedades asignadas en línea al elemento (con el atributo style) o mediante element.style.propiedad.
- Para modificar los estilos de múltiples elementos, se puede inyectar desde JavaScript elementos de tipo style o añadir reglas a una hoja existente.
var e = document.createElement("style");
e.innerHTML= "p {color: red;}"
document.head.appendChild(e);
var e= document.querySelector('link[href="normal.css"]')
e.sheet.insertRule("p { background-color: blue}"); // otra manera
Recorrer el DOM
var bodyElement = document.body;
if (bodyElement.firstChild) {
...
}
var bodyElement = document.body;
for (var i = 0; i < bodyElement.children.length; i++) {
var childElement = bodyElement.children[i];
console.log(childElement.tagName);
}
function theDOMElementWalker(node) {
if (node.nodeType == Node.ELEMENT_NODE) {
// console.log(node.tagName);
node = node.firstChild;
while (node) {
theDOMElementWalker(node);
node = node.nextSibling;
}
}
}
Mejor que usar innerHTML
- Crear el elemento con document.createElement; esta función solo está definida para document.
- Obtener una referencia al nodo que hará de padre.
- Insertar el nuevo nodo con appendChild.
<body>
<h1 id="theTitle" class="highlight summer">What's happening?</h1>
<script>
var newElement = document.createElement("p");
newElement.textContent = "I exist entirely in your imagination.";
document.body.appendChild(newElement);
</script>
</body>
innerHTML y textContent
- innerHTML analiza (parsing) el contenido como HTML y tarda más.
- textContent interpreta el contenido como texto plano, es más rápido y previene ataques XSS (cross-site scripting)
Inserción en el DOM
- appendChild siempre añade el nuevo elemento como último hijo del padre.
- Para insertarlo en otra posición hay que usar insertBefore.
<body>
<h1 id="theTitle" class="highlight summer">What's happening?</h1>
<script>
var newElement = document.createElement("p");
newElement.textContent = "I exist entirely in your imagination.";
var scriptElement = document.querySelector("script");
document.body.insertBefore(newElement, scriptElement);
document.body.removeChild(newElement); // borrado del elemento
// newElement.parentNode.removeChild(newElement);
// equivalente para cuando no tenemos un objeto apuntando al padre
</script>
</body>
Gestión de eventos
- Eventos típicos son click, mouseover, DOMContentLoaded (cuando el DOM se ha cargado), load (cuando todo el documento se ha cargado), keydown, keyup…
<!doctype html>
<html>
<head>
<title>Click Anywhere!</title>
</head>
<body>
<script>
document.addEventListener("click", changeColor, false);
function changeColor() {
document.body.style.backgroundColor = "#FFC926";
}
</script>
</body>
</html>
Gestión de eventos
- El manejador de eventos es invocado por el navegador con un parámetro de tipo evento.
- e.currentTarget es el elemento al que se asoció el manejador, mientras que e.target es el elemento sobre el que ha ocurrido el evento.
function hide(e){
e.currentTarget.style.visibility = "hidden";
}
var ps = document.getElementsByTagName('p');
for(var i = 0; i < ps.length; i++){
ps[i].addEventListener('click', hide, false);
}
// click around and make paragraphs disappear
Captura y burbujeo de eventos
![]()
Fuente
Captura y burbujeo de eventos
- Los eventos comienzan por la raíz del árbol y se van propagando hacia abajo hasta llegar al elemento en que se produjo.
- En una segunda fase burbujean (to bubble) hacia arriba.
- En cada paso, se invoca a los manejadores de eventos que hayan sido definidos.
- El tercer parámetro de addEventListener es un booleano que define si el manejador de evento es para la fase de captura (o la de bubbling si es false).
- e.stopPropagation() detiene el proceso en el manejador actual.
Captura y burbujeo de eventos
<!doctype html>
<html>
<head>
<title>Eventos</title>
<style>
div {border: 1px solid black; padding: 20px; margin: 10px;}
</style>
</head>
<body id="theBody" class="item">
<div id="one_a" class="item">
<div id="two" class="item">
<div id="three_a" class="item">
<button id="buttonOne" class="item">one</button>
</div>
<div id="three_b" class="item">
<button id="buttonTwo" class="item">two</button>
<button id="buttonThree" class="item">three</button>
</div>
</div>
</div>
<div id="one_b" class="item">
</div>
<script>
var items = document.querySelectorAll(".item");
for (var i = 0; i < items.length; i++) {
var el = items[i];
//capturing phase
el.addEventListener("click", doSomething, true);
//bubbling phase
el.addEventListener("click", doSomething, false);
}
function doSomething(e) {
console.log(e.currentTarget.id + " (target:"+e.target.id+")");
}
</script>
</body>
</html>
El bucle de eventos
- La ejecución del código de un programa en JavaScript se produce en un único hilo (normalmente, uno por cada pestaña del navegador).
- Este hilo se puede bloquear (de manera que la aplicación del navegador deja de responder) si se ejecuta código muy largo o un bucle infinito.
- Muchas funciones de la API web que tienen que ver con entrada/salida son no bloqueantes / asíncronas y se ejecutan en otro hilo.
function timeout() {
timeoutID = setTimeout(f, 2000);
}
// función de callback
function f() {...}
El bucle de eventos
- Pese a ejecutarse el código en un único hilo, el hilo principal de ejecución no se bloquea mientras las funciones de la API que hemos estudiado (por ejemplo, setTimeout o addEventListener) están esperando al evento correspondiente que disparará la ejecución de la función de callback.
- Cuando una función asíncrona de callback de la API web se tiene que ejecutar (por ejemplo, porque termina la cuenta atrás o porque se hace clic en un botón), la llamada correspondiente no se ejecuta inmediatamente, sino que se encola.
El bucle de eventos
- El bucle de eventos atiende y ejecuta las funciones almacenadas en la cola de funciones callback.
- El motor de JavaScript no procesa el bucle de eventos hasta que el código del callback actual termina.
- En general, el bucle de eventos no saca ninguna función de callback de la cola hasta que la pila esté vacía.
- Herramienta para visualizar todo lo anterior (un ejemplo, otro).
Gestión de memoria
- Cada hilo de JavaScript utiliza una pila y un heap de forma similar a otros lenguajes de programación.
- Existe también un espacio de memoria para las variables globales.
- El recolector de basura funciona de forma similar a otros lenguajes como Java, eliminando objetos de memoria cuando no son alcanzables.
- La tecnología de web workers permite definir código que se ejecuta en su propio hilo.
Cuando se carga una página
- El navegador va construyendo el DOM conforme va analizando el documento HTML.
- Cuando encuentra un elemento script el analizador se detiene y se procesa el código en JavaScript; si este código contiene instrucciones fuera de funciones se ejecutan.
- Tras procesar un bloque script el navegador continúa analizando el HTML subsiguiente.
- Este bloqueo por defecto del analizador de HTML puede resultar en una mala experiencia de usuario (demasiado tiempo esperando la carga de la página); por ello, se puede sobrescribir con los atributos async y defer.
Los atributos async y defer
- Los scripts marcados con uno de los atributos async o defer no detienen el análisis y visualización del documento; los scripts se descargan en segundo plano.
- Un script marcado con async se puede comenzar a descargar en cualquier momento y cuando se ha descargado se ejecuta; el evento DOMContentLoaded puede producirse antes o después de dicha ejecución; otros script marcados con async podrían ejecutarse antes de otro script marcado con async que aparece antes en el código.
Los atributos async y defer
- Un script marcado con defer se ejecuta cuando el árbol está listo, pero antes de disparar el evento DOMContentLoaded; los scripts marcados con defer se ejecutan en el orden en el que aparecen en el documento.
Dónde poner el JavaScript
- Antiguamente se recomendaba colocar los scripts al final del body (o, en otras partes del documento, pero esperando al evento DOMContentLoaded para usar funciones como querySelector), pero esto introduce una secuenciación que puede resultar en una mala experiencia de usuario (la página se visualiza, pero no se puede interactuar sobre ella porque el código se está descargando todavía).
Dónde poner el JavaScript
- La práctica moderna es colocar los scripts en la cabecera (head) y usar async o defer según corresponda; así, los scripts se descargan asíncronamente sin bloquear el navegador.
- Para los scripts async es necesario seguir esperando al evento DOMContentLoaded antes de intentar recorrer el árbol DOM.
<script defer src="j1.js"></script> // En HTML5 no es necesario usar type="text/javascript"
<script async src="j2.js"></script>
<script src="j3.js"></script>