Cómo funcionan los navegadores

El detrás de escena de los navegadores web modernos

Prefacio

Este manual básico sobre operaciones internas de WebKit y Gecko es resultado de una gran investigación de la empresa israelí Tali Garsiel. En unos pocos durante varios años, revisó todos los datos publicados sobre componentes internos del navegador y dedicó mucho tiempo leyendo el código fuente del navegador web. Escribió:

Como desarrollador web, conocer los aspectos internos de las operaciones del navegador Te ayuda a tomar mejores decisiones y a conocer las justificaciones detrás del desarrollo. prácticas recomendadas. Si bien este documento es bastante extenso, recomendamos que dediques un poco de tiempo a investigar. Te alegrarás de haberlo hecho.

Paul Ireland, Relaciones con Desarrolladores de Chrome

Introducción

Los navegadores web son el software más utilizado. En este manual, explico cómo trabajan detrás de escena. Veremos lo que sucede cuando escribes google.com en la barra de direcciones hasta que veas la página de Google en la pantalla del navegador.

Navegadores de los que hablaremos

En la actualidad, se utilizan cinco navegadores principales en las computadoras de escritorio: Chrome, Internet Explorer, Firefox, Safari y Opera. En los dispositivos móviles, los principales navegadores son el navegador Android, iPhone, Opera Mini y Opera Mobile, el navegador UC, los navegadores Nokia S40/S60 y Chrome. Todos estos navegadores, excepto los Opera, se basan en WebKit. Proporcionaré ejemplos de los navegadores de código abierto Firefox y Chrome, y Safari (que es parcialmente de código abierto). Según las estadísticas de StatCounter (hasta junio de 2013), Chrome, Firefox y Safari representan alrededor del 71% del uso global de navegadores de escritorio. En los dispositivos móviles, el navegador Android, iPhone y Chrome constituyen alrededor del 54% del uso.

La funcionalidad principal del navegador

La función principal de un navegador es presentar el recurso web que elijas solicitándolo al servidor y mostrándolo en la ventana del navegador. Por lo general, el recurso es un documento HTML, pero también puede ser un PDF, una imagen o algún otro tipo de contenido. El usuario especifica la ubicación del recurso mediante un URI (identificador uniforme de recursos).

La manera en que el navegador interpreta y muestra los archivos HTML se especifica en las especificaciones HTML y CSS. La organización W3C (World Wide Web Consortium), que es la organización de estándares para la Web, mantiene estas especificaciones. Durante años, los navegadores se adaptaron a solo una parte de las especificaciones y desarrollaron sus propias extensiones. Eso provocó graves problemas de compatibilidad para los autores web. En la actualidad, la mayoría de los navegadores se ajustan en mayor o menor medida a las especificaciones.

Las interfaces de usuario de los navegadores tienen mucho en común. Entre los elementos comunes de la interfaz de usuario, se encuentran los siguientes:

  1. Barra de direcciones para insertar un URI
  2. Botones para retroceder y avanzar
  3. Opciones para agregar a favoritos
  4. Botones de actualización y detención para actualizar o detener la carga de documentos actuales
  5. Botón de inicio que te lleva a la página principal

Curiosamente, la interfaz de usuario del navegador no está especificada en ninguna especificación formal, sino que proviene de buenas prácticas formadas a lo largo de años de experiencia y de que los navegadores se imitan entre sí. La especificación HTML5 no define los elementos de la interfaz de usuario que debe tener un navegador, sino que enumera algunos elementos comunes. Entre ellas, se encuentran la barra de direcciones, la barra de estado y la barra de herramientas. Por supuesto, existen funciones exclusivas de un navegador específico, como el administrador de descargas de Firefox.

Infraestructura de alto nivel

Los componentes principales del navegador son los siguientes:

  1. Interfaz de usuario: Incluye la barra de direcciones, el botón Atrás/Adelante, el menú de favoritos, etc. Se muestran todas las partes del navegador, excepto la ventana donde ves la página solicitada.
  2. El motor del navegador: Ordena las acciones entre la IU y el motor de renderización.
  3. El motor de renderización: Es responsable de mostrar el contenido solicitado. Por ejemplo, si el contenido solicitado es HTML, el motor de renderización analiza HTML y CSS, y muestra el contenido analizado en la pantalla.
  4. Herramientas de redes: Para llamadas de red, como solicitudes HTTP, mediante diferentes implementaciones para diferentes plataformas detrás de una interfaz independiente de la plataforma.
  5. Backend de la IU: Se usa para dibujar widgets básicos, como ventanas y cuadros combinados. Este backend expone una interfaz genérica que no es específica de la plataforma. Debajo, se usan los métodos de la interfaz de usuario del sistema operativo.
  6. Intérprete de JavaScript. Se usa para analizar y ejecutar código JavaScript.
  7. Almacenamiento de datos. Esta es una capa de persistencia. Es posible que el navegador deba guardar todo tipo de datos de forma local, como las cookies. Los navegadores también admiten mecanismos de almacenamiento como localStorage, IndexedDB, WebSQL y FileSystem.
Componentes del navegador
Figura 1: Componentes del navegador
.

Es importante tener en cuenta que los navegadores como Chrome ejecutan varias instancias del motor de renderización: una para cada pestaña. Cada pestaña se ejecuta en un proceso independiente.

Motores de renderización

La responsabilidad del motor de renderización es bien... la renderización, que es mostrar el contenido solicitado en la pantalla del navegador.

De forma predeterminada, el motor de procesamiento puede mostrar imágenes y documentos HTML y XML. Puede mostrar otros tipos de datos a través de complementos o extensiones. por ejemplo, para mostrar documentos PDF con un complemento de lector de PDF. Sin embargo, en este capítulo nos enfocaremos en el caso de uso principal: mostrar imágenes y HTML con formato CSS.

Los diferentes navegadores utilizan distintos motores de representación: Internet Explorer usa Trident, Firefox usa Gecko y Safari usa WebKit. Chrome y Opera (de la versión 15) utilizan Blink, una horquilla de WebKit.

WebKit es un motor de renderización de código abierto que comenzó como un motor para la plataforma Linux y que Apple modificó para ser compatible con Mac y Windows.

El flujo principal

El motor de renderización comenzará a obtener el contenido del documento solicitado. de la capa de red. Por lo general, esto se hace en fragmentos de 8 KB.

A continuación, este es el flujo básico del motor de renderización:

Flujo básico del motor de renderización
Figura 2: Flujo básico del motor de renderización

El motor de renderización comenzará a analizar el documento HTML y convertirá los elementos en nodos DOM en un árbol denominado "árbol de contenido". El motor analizará los datos de estilo, tanto en archivos CSS externos como en elementos de estilo. La información de estilo junto con las instrucciones visuales en el código HTML se usará para crear otro árbol: el árbol de renderización.

El árbol de renderización contiene rectángulos con atributos visuales, como el color y las dimensiones. Los rectángulos están en el orden correcto para que se muestren en la pantalla.

Después de la construcción del árbol de renderización, se pasa por un “diseño”. el proceso de administración de recursos. Esto significa que debes indicar a cada nodo las coordenadas exactas del lugar en el que debe aparecer en la pantalla. La siguiente etapa es Painting: Se recorrerá el árbol de renderización y se pintará cada nodo con la capa de backend de la IU.

Es importante que entiendas que este es un proceso gradual. Para mejorar la experiencia del usuario, el motor de renderización intentará mostrar contenido en la pantalla lo antes posible. No esperará hasta que se analice todo el HTML para comenzar a compilar y diseñar el árbol de representación. Se analizarán y se mostrarán partes del contenido, mientras que el proceso continúa con el resto del contenido que continúa procediendo de la red.

Ejemplos de flujo principal

Flujo principal de WebKit
Figura 3: Flujo principal de WebKit
Flujo principal del motor de renderización Gecko de Mozilla.
Figura 4: Flujo principal del motor de renderización Gecko de Mozilla

En las figuras 3 y 4, se puede ver que aunque WebKit y Gecko usan terminología ligeramente diferente, el flujo es básicamente el mismo.

Gecko denomina "árbol de marcos" al árbol de elementos con formato visual. Cada elemento es un marco. WebKit usa el término “árbol de renderización” y consta de “Objetos de renderización”. WebKit utiliza el término “diseño” para la colocación de elementos, mientras que Gecko lo llama "Reprocesamiento". "Archivo adjunto" es el término de WebKit para conectar nodos del DOM y la información visual para crear el árbol de representación. Una pequeña diferencia no semántica es que Gecko tiene una capa adicional entre el HTML y el árbol del DOM. Se denomina “receptor de contenido”. y es una fábrica para crear elementos del DOM. Hablaremos sobre cada parte del flujo:

Análisis: general

Dado que el análisis es un proceso muy importante dentro del motor de renderización, lo ahondaremos un poco más en detalle. Comencemos con una pequeña introducción al análisis.

Analizar un documento significa traducirlo a una estructura que el código pueda usar. Por lo general, el resultado del análisis es un árbol de nodos que representa la estructura del documento. Esto se denomina árbol de análisis o árbol de sintaxis.

Por ejemplo, si analizas la expresión 2 + 3 - 1, se podría mostrar este árbol:

Nodo de árbol de expresión matemática
Figura 5: Nodo de árbol de expresiones matemáticas

Gramática

El análisis se basa en las reglas sintácticas que obedece en el documento: el lenguaje o el formato en el que se escribió. Cada formato que puedas analizar debe tener una gramática determinística que conste de reglas de vocabulario y sintaxis. Se denomina gramática sin contexto. Los lenguajes humanos no son esos idiomas y, por lo tanto, no se pueden analizar con técnicas de análisis convencionales.

Combinación de Lexer y analizador

El análisis se puede separar en dos subprocesos: análisis léxico y análisis sintáctico.

El análisis léxico es el proceso de dividir la entrada en tokens. Los tokens son el vocabulario del lenguaje: la colección de componentes básicos válidos. En el lenguaje humano, constará de todas las palabras que aparecen en el diccionario de ese idioma.

El análisis sintáctico es la aplicación de las reglas de sintaxis del lenguaje.

Los analizadores suelen dividir el trabajo entre dos componentes: el lexer (a veces llamado tokenizador) que es responsable de dividir la entrada en tokens válidos y el analizador que construye el árbol de análisis analizando la estructura del documento de acuerdo con las reglas de sintaxis del lenguaje.

Sabe cómo quitar caracteres irrelevantes, como espacios en blanco y saltos de línea.

Del documento fuente al análisis de árboles
Figura 6: Del documento fuente al análisis de árboles

El proceso de análisis es iterativo. Por lo general, el analizador le pedirá al lexer un token nuevo y tratará de hacer coincidir el token con una de las reglas de sintaxis. Si una regla coincide, se agregará un nodo correspondiente al token al árbol de análisis y el analizador solicitará otro token.

Si no coincide ninguna regla, el analizador almacenará el token internamente y seguirá solicitando tokens hasta que se encuentre una regla que coincida con todos los tokens almacenados internamente. Si no se encuentra ninguna regla, el analizador presentará una excepción. Esto significa que el documento no era válido y contenía errores de sintaxis.

Traducción

En muchos casos, el árbol de análisis no es el producto final. El análisis suele usarse en la traducción: transformar el documento de entrada a otro formato. Un ejemplo es la compilación. El compilador que compila el código fuente en código máquina primero lo analiza en un árbol de análisis y, luego, traduce el árbol en un documento de código máquina.

Flujo de compilación
Figura 7: Flujo de compilación

Ejemplo de análisis

En la figura 5, construimos un árbol de análisis a partir de una expresión matemática. Intentemos definir un lenguaje matemático simple y veamos el proceso de análisis.

Sintaxis:

  1. Los componentes básicos de la sintaxis del lenguaje son las expresiones, los términos y las operaciones.
  2. Nuestro lenguaje puede incluir cualquier cantidad de expresiones.
  3. Una expresión se define como un “término” seguida de una “operación” seguido de otro término
  4. Una operación es un token más o menos
  5. Un término es un token de número entero o una expresión

Analicemos el 2 + 3 - 1 de entrada.

La primera subcadena que coincide con una regla es 2: según la regla n.o 5, es un término. La segunda coincidencia es 2 + 3: coincide con la tercera regla: un término seguido de una operación seguido de otro término. La siguiente coincidencia solo se alcanzará al final de la entrada. 2 + 3 - 1 es una expresión porque ya sabemos que 2 + 3 es un término, por lo que tenemos un término seguido de una operación seguido de otro término. 2 + + no coincidirá con ninguna regla, por lo que no es una entrada válida.

Definiciones formales de vocabulario y sintaxis

El vocabulario se suele expresar mediante expresiones regulares.

Por ejemplo, nuestro lenguaje se definirá de la siguiente manera:

INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -

Como puedes ver, los números enteros se definen a través de una expresión regular.

Por lo general, la sintaxis se define en un formato llamado BNF. Nuestro lenguaje se definirá de la siguiente manera:

expression :=  term  operation  term
operation :=  PLUS | MINUS
term := INTEGER | expression

Dijimos que los analizadores regulares pueden analizar un idioma si su gramática no tiene contexto. Una definición intuitiva de una gramática libre de contexto es una gramática que puede expresarse completamente en BNF. Para ver una definición formal, consulta Artículo de Wikipedia sobre gramática sin contexto

Tipos de analizadores

Existen dos tipos de analizadores: los de arriba abajo y los de arriba abajo. Una explicación intuitiva es que los analizadores de Top Down examinan la estructura de alto nivel de la sintaxis y buscan una coincidencia de reglas. Los analizadores ascendentes comienzan con la entrada y la transforman gradualmente en reglas de sintaxis, comenzando desde las reglas de bajo nivel hasta que se cumplen las reglas de alto nivel.

Veamos cómo los dos tipos de analizadores analizarán nuestro ejemplo.

El analizador de arriba abajo comenzará a partir de la regla de nivel superior: identificará 2 + 3 como una expresión. Luego, identificará 2 + 3 - 1 como una expresión (el proceso de identificación de la expresión evoluciona y coincide con las otras reglas, pero el punto de partida es la regla de nivel más alto).

El analizador Bottom Up analizará la entrada hasta que se detecte una coincidencia con una regla. Luego, reemplazará la entrada coincidente con la regla. Esto continuará hasta el final de la entrada. La expresión parcialmente coincidente se coloca en la pila del analizador.

Apilar Entrada
2 + 3 - 1
término + 3 - 1
operación de término 3 - 1
expresión : 1
operación de expresión 1
expresión -

Este tipo de analizador de abajo hacia arriba se denomina analizador de mayúsculas y minúsculas porque la entrada se desplaza hacia la derecha (imagina un puntero que apunta primero al inicio de la entrada y se mueve hacia la derecha) y se reduce gradualmente a reglas de sintaxis.

Genera analizadores automáticamente

Existen herramientas que pueden generar un analizador. Les das la gramática de tu lenguaje (su vocabulario y reglas de sintaxis) y generan un analizador que funciona. Crear un analizador requiere una comprensión profunda del análisis y no es fácil crear uno optimizado de forma manual, por lo que los generadores de analizadores pueden ser muy útiles.

WebKit usa dos generadores de analizadores conocidos: Flex para crear un analizador y Bison para crear un analizador (es posible que te encuentres con ellos con los nombres Lex y Yacc). La entrada de Flex es un archivo que contiene definiciones de expresiones regulares de los tokens. La entrada de Bison son las reglas de sintaxis del lenguaje en formato BNF.

Analizador HTML

El trabajo del analizador de HTML es analizar el lenguaje de marcado HTML en un árbol de análisis.

Gramática HTML

El vocabulario y la sintaxis de HTML se definen en especificaciones creadas por la organización W3C.

Como vimos en la introducción del análisis, la sintaxis gramatical se puede definir formalmente con formatos como BNF.

Lamentablemente, todos los temas de los analizadores convencionales no se aplican al código HTML (no los mencioné por diversión, sino que se usarán para analizar CSS y JavaScript). HTML no se puede definir fácilmente mediante una gramática libre de contexto que los analizadores necesitan.

Existe un formato formal para definir HTML, DTD (definición del tipo de documento), pero no es una gramática sin contexto.

Esto parece extraño a primera vista; HTML es bastante parecido a XML. Hay muchos analizadores de XML disponibles. Existe una variación XML de HTML y XHTML. Entonces, ¿cuál es la gran diferencia?

La diferencia es que el enfoque HTML es más "tolerante": te permite omitir ciertas etiquetas (que luego se agregan implícitamente) u, a veces, omitir las etiquetas de inicio o finalización, y así sucesivamente. En general, es un tono "suave" en lugar de la sintaxis rígida y exigente del XML.

Este detalle aparentemente pequeño marca una gran diferencia. Por un lado, éste es el motivo principal por el que HTML es tan popular: perdona sus errores y facilita la vida del autor del sitio web. Por otro lado, dificulta la escritura gramatical formal. En resumen, los analizadores convencionales no pueden analizar fácilmente HTML, ya que su gramática no está libre de contexto. Los analizadores de XML no pueden analizar HTML.

DTD de HTML

La definición HTML está en formato DTD. Este formato se usa para definir lenguajes de la familia SGML. El formato contiene definiciones para todos los elementos permitidos, sus atributos y jerarquía. Como vimos antes, la DTD de HTML no crea una gramática libre de contexto.

Existen algunas variaciones de la DTD. El modo estricto se ajusta únicamente a las especificaciones, pero otros modos admiten el lenguaje de marcado que usaban los navegadores anteriormente. El propósito es la retrocompatibilidad con el contenido más antiguo. El DTD estricto actual está aquí: www.w3.org/TR/html4/strict.dtd

DOM

El árbol de resultados (el "árbol de análisis") es un árbol de nodos de elementos y atributos del DOM. DOM es la abreviatura de Document Object Model. Es la presentación de objetos del documento HTML y la interfaz de los elementos HTML al mundo exterior, como JavaScript.

La raíz del árbol se denomina “Document”. .

El DOM tiene una relación casi uno a uno con el lenguaje de marcado. Por ejemplo:

<html>
  <body>
    <p>
      Hello World
    </p>
    <div> <img src="example.png"/></div>
  </body>
</html>

Este lenguaje de marcado se traduciría al siguiente árbol del DOM:

Árbol del DOM del lenguaje de marcado de ejemplo
Figura 8: Árbol del DOM del lenguaje de marcado de ejemplo

Al igual que HTML, el DOM es especificado por la organización W3C. Consulta www.w3.org/DOM/DOMTR. Es una especificación genérica para manipular documentos. Un módulo específico describe elementos HTML específicos. Puedes encontrar las definiciones de HTML aquí: www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html.

Cuando digo que el árbol contiene nodos del DOM, me refiero a que el árbol está construido con elementos que implementan una de las interfaces del DOM. Los navegadores usan implementaciones concretas que tienen otros atributos que el navegador utiliza internamente.

El algoritmo de análisis

Como vimos en las secciones anteriores, el HTML no se puede analizar con los analizadores de arriba abajo normales.

Estos son los motivos:

  1. La naturaleza tolerante del lenguaje.
  2. El hecho de que los navegadores tengan la tolerancia a errores tradicional para admitir casos conocidos de HTML no válido.
  3. El proceso de análisis es reentrante. En otros idiomas, la fuente no cambia durante el análisis, pero en HTML, el código dinámico (como los elementos de secuencia de comandos que contienen llamadas document.write()) puede agregar tokens adicionales, por lo que el proceso de análisis en realidad modifica la entrada.

No se pueden usar las técnicas de análisis habituales, los navegadores crean analizadores personalizados para analizar HTML.

El algoritmo de análisis se describe en detalle en la especificación de HTML5. El algoritmo consta de dos etapas: asignación de token y construcción de árbol.

La asignación de token es el análisis léxico, en el que se analiza la entrada en tokens. Entre los tokens HTML se encuentran las etiquetas de inicio y de finalización, y los nombres y valores de atributos.

El tokenizador reconoce el token, se lo da al constructor del árbol y consume el carácter siguiente para reconocerlo, y así sucesivamente hasta el final de la entrada.

Flujo de análisis de HTML (tomado de las especificaciones de HTML5)
Figura 9: Flujo de análisis de HTML (tomado de las especificaciones de HTML5)

El algoritmo de asignación de token

La salida del algoritmo es un token HTML. El algoritmo se expresa como una máquina de estados. Cada estado consume uno o más caracteres del flujo de entrada y actualiza el siguiente estado de acuerdo con esos caracteres. La decisión está influenciada por el estado actual de la asignación de token y por el estado de construcción del árbol. Esto significa que el mismo carácter consumido producirá resultados diferentes para el siguiente estado correcto, según el estado actual. El algoritmo es demasiado complejo para describirlo por completo, así que veamos un ejemplo simple que nos ayudará a comprender el principio.

Ejemplo básico: asignación de tokens al siguiente HTML:

<html>
  <body>
    Hello world
  </body>
</html>

El estado inicial es el "Estado de los datos". Cuando se encuentra el carácter <, el estado cambia a "Tag open state". Consumir un carácter a-z provoca la creación de un "Token de etiqueta de inicio"; el estado cambia a "Tag name state". Permanecemos en este estado hasta que se consume el carácter >. Cada carácter se agrega al nuevo nombre del token. En nuestro caso, el token creado es un token html.

Cuando se alcanza la etiqueta >, se emite el token actual, y el estado vuelve a cambiar al "Estado de los datos". La etiqueta <body> se tratará en los mismos pasos. Hasta ahora, se emitieron las etiquetas html y body. Ahora estamos de vuelta en el “Estado de los datos”. Consumir el carácter H de Hello world hará que se cree y emita un token de carácter. Esto continuará hasta que se alcance el < de </body>. Emitiremos un token de carácter para cada carácter de Hello world.

Ahora volvemos al "Estado de la etiqueta abierta". Consumir la siguiente entrada / hará que se cree una end tag token y se cambie al "Estado del nombre de la etiqueta". Una vez más, permanecemos en este estado hasta llegar a >.Luego, se emitirá el nuevo token de etiqueta y regresaremos al "Estado de los datos". La entrada </html> se tratará como el caso anterior.

Asigna tokens a la entrada de ejemplo
Figura 10: Asignación de tokens a la entrada de ejemplo

Algoritmo de construcción de árboles

Cuando se crea el analizador, también se crea el objeto Document. Durante la etapa de construcción del árbol, se modificará el árbol del DOM con el Documento en su raíz y se le agregarán elementos. El constructor del árbol procesará cada nodo emitido por el tokenizador. Para cada token, la especificación define qué elemento del DOM es relevante para él y se creará para este token. El elemento se agrega al árbol del DOM y también a la pila de elementos abiertos. Esta pila se usa para corregir discrepancias de anidación y etiquetas no cerradas. El algoritmo también se describe como una máquina de estados. Los estados se llaman “modos de inserción”.

Veamos el proceso de construcción del árbol para la entrada de ejemplo:

<html>
  <body>
    Hello world
  </body>
</html>

La entrada a la etapa de construcción del árbol es una secuencia de tokens de la etapa de asignación de token. El primer modo es el "modo inicial". Recibir el "html" hará que el token se mueva al modo "before html" y se vuelva a procesar en ese modo. Esto provocará la creación del elemento HTMLHtmlElement, que se agregará al objeto Document raíz.

El estado cambiará a "before head". El "cuerpo" y, luego, se recibe el token. Se creará un HTMLHeadElement de manera implícita, aunque no tengamos un encabezado. token y se agregará al árbol.

Ahora pasamos al modo “en la cabeza” y, luego, a “después de la cabeza”. El token de cuerpo se vuelve a procesar, se crea e inserta un HTMLBodyElement, y el modo se transfiere a "in body".

Los tokens de caracteres de "Hello World" una cadena de caracteres. El primero provocará la creación y la inserción de un "Texto" nodo y los demás caracteres se agregarán a ese nodo.

La recepción del token de finalización del cuerpo provocará una transferencia al modo "after body". Ahora recibiremos la etiqueta html de cierre, que nos moverá al modo "after after body". Si recibes el token de fin de archivo, se finalizará el análisis.

Construcción de árbol del HTML de ejemplo.
Figura 11: Construcción de árbol del código HTML de ejemplo

Acciones cuando finaliza el análisis

En esta etapa, el navegador marcará el documento como interactivo y comenzará a analizar las secuencias de comandos que estén en "diferido" : las que deben ejecutarse después de analizar el documento. El estado del documento se establecerá como "completo". y una "carga" evento activado.

Puede ver los algoritmos completos para la asignación de token y la construcción de árboles en la especificación de HTML5.

Navegadores tolerancia a errores

Nunca obtendrás una "sintaxis no válida" en una página HTML. Los navegadores corrigen el contenido no válido y continúan.

Tomemos este HTML por ejemplo:

<html>
  <mytag>
  </mytag>
  <div>
  <p>
  </div>
    Really lousy HTML
  </p>
</html>

Debo haber incumplido alrededor de un millón de reglas ("mytag" no es una etiqueta estándar, anidamiento incorrecto de los elementos "p" y "div", entre otros), pero el navegador lo muestra correctamente y no presenta ningún reclamo. Por lo tanto, gran parte del código del analizador corrige los errores del autor HTML.

El manejo de errores es bastante consistente en los navegadores, pero sorprendentemente no ha formado parte de las especificaciones HTML. Como los marcadores y los botones atrás/adelante, es algo que se desarrolló en los navegadores a lo largo de los años. Se conocen construcciones HTML no válidas que se repiten en muchos sitios y los navegadores intentan corregirlas de manera que se ajusten a los otros navegadores.

La especificación HTML5 define algunos de estos requisitos. (WebKit resume esto muy bien en el comentario al comienzo de la clase de analizador de HTML).

El analizador analiza las entradas con asignación de token en el documento y crea el árbol de documentos. Si el documento tiene el formato correcto, es sencillo analizarlo.

Lamentablemente, tenemos que manejar muchos documentos HTML que no tienen el formato correcto, por lo que el analizador debe tolerar los errores.

Debemos ocuparnos de al menos las siguientes condiciones de error:

  1. El elemento que se agrega está explícitamente prohibido dentro de alguna etiqueta externa. En este caso, debemos cerrar todas las etiquetas hasta la que prohíbe el elemento y, luego, agregarlas.
  2. No podemos agregar el elemento directamente. Esto puede deberse a que la persona que escribió el documento olvidó alguna etiqueta intermedia (o que la etiqueta intermedia es opcional). Esto podría suceder con las siguientes etiquetas: HTML HEAD BODY TBODY TR TD LI (¿olvidé alguna?).
  3. Queremos agregar un elemento de bloque dentro de un elemento intercalado. Cierra todos los elementos intercalados hasta el siguiente elemento de bloque más alto.
  4. Si esto no ayuda, cierra los elementos hasta que podamos agregarlos o ignora la etiqueta.

Veamos algunos ejemplos de tolerancia a errores de WebKit:

</br> en lugar de <br>

Algunos sitios usan </br> en lugar de <br>. Para ser compatible con IE y Firefox, WebKit lo trata como <br>.

El código:

if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
     reportError(MalformedBRError);
     t->beginTag = true;
}

Ten en cuenta que el manejo de errores es interno, no se presentará al usuario.

Una tabla desviada

Una tabla desviada es una tabla que está dentro de otra tabla, pero no dentro de una celda.

Por ejemplo:

<table>
  <table>
    <tr><td>inner table</td></tr>
  </table>
  <tr><td>outer table</td></tr>
</table>

WebKit cambiará la jerarquía a dos tablas del mismo nivel:

<table>
  <tr><td>outer table</td></tr>
</table>
<table>
  <tr><td>inner table</td></tr>
</table>

El código:

if (m_inStrayTableContent && localName == tableTag)
        popBlock(tableTag);

WebKit utiliza una pila para el contenido del elemento actual: quitará la tabla interna de la pila externa. Las tablas ahora serán del mismo nivel.

Elementos de formulario anidados

Si el usuario coloca un formulario dentro de otro formulario, se ignora el segundo.

El código:

if (!m_currentFormElement) {
        m_currentFormElement = new HTMLFormElement(formTag,    m_document);
}

Una jerarquía de etiquetas demasiado profunda

El comentario habla por sí mismo.

bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName)
{

unsigned i = 0;
for (HTMLStackElem* curr = m_blockStack;
         i < cMaxRedundantTagDepth && curr && curr->tagName == tagName;
     curr = curr->next, i++) { }
return i != cMaxRedundantTagDepth;
}

Etiquetas de cierre del cuerpo o del código HTML mal colocados

Una vez más, el comentario habla por sí mismo.

if (t->tagName == htmlTag || t->tagName == bodyTag )
        return;

Por lo tanto, los autores web deben tener cuidado, a menos que quieran aparecer como ejemplo en un fragmento de código de tolerancia a errores de WebKit, escriban código HTML con formato válido.

Análisis de CSS

¿Recuerdas los conceptos de análisis en la introducción? Bueno, a diferencia de HTML, CSS es una gramática sin contexto y se puede analizar usando los tipos de analizadores descritos en la introducción. De hecho, la especificación de CSS define la gramática léxica y de sintaxis de CSS.

Veamos algunos ejemplos:

La gramática léxica (vocabulario) se define a través de expresiones regulares para cada token:

comment   \/\*[^*]*\*+([^/*][^*]*\*+)*\/
num       [0-9]+|[0-9]*"."[0-9]+
nonascii  [\200-\377]
nmstart   [_a-z]|{nonascii}|{escape}
nmchar    [_a-z0-9-]|{nonascii}|{escape}
name      {nmchar}+
ident     {nmstart}{nmchar}*

&quot;ident&quot; es la forma abreviada de identificador, como un nombre de clase. "nombre" es un ID de elemento (al que se hace referencia con "#").

La gramática de la sintaxis se describe en BNF.

ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;
selector
  : simple_selector [ combinator selector | S+ [ combinator? selector ]? ]?
  ;
simple_selector
  : element_name [ HASH | class | attrib | pseudo ]*
  | [ HASH | class | attrib | pseudo ]+
  ;
class
  : '.' IDENT
  ;
element_name
  : IDENT | '*'
  ;
attrib
  : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
    [ IDENT | STRING ] S* ] ']'
  ;
pseudo
  : ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
  ;

Explicación:

Un conjunto de reglas es la siguiente estructura:

div.error, a.error {
  color:red;
  font-weight:bold;
}

div.error y a.error son selectores. La parte dentro de las llaves contiene las reglas que aplica este conjunto de reglas. Esta estructura se define formalmente en esta definición:

ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;

Esto significa que un conjunto de reglas es un selector o, opcionalmente, varios selectores separados por una coma y espacios (S significa espacio en blanco). Un conjunto de reglas contiene llaves y, dentro de ellas, una declaración o, opcionalmente, varias declaraciones separadas por punto y coma. “declaración” y "selector" se definirán en las siguientes definiciones de BNF.

Analizador de CSS de WebKit

WebKit usa generadores de analizadores Flex y Bison para crear analizadores automáticamente a partir de los archivos de gramática de CSS. Como recuerdas de la introducción al analizador, Bison crea un analizador de tipo Mayúsculas abajo hacia arriba. Firefox usa un analizador de Top Down escrito manualmente. En ambos casos, cada archivo CSS se analiza como un objeto StyleSheet. Cada objeto contiene reglas CSS. Los objetos de la regla de CSS contienen objetos selectores y de declaración, además de otros objetos correspondientes a la gramática de CSS.

Análisis de CSS
Figura 12: Análisis de CSS

Orden de procesamiento de las secuencias de comandos y las hojas de estilo

Secuencias de comandos

El modelo de la Web es síncrono. Los autores esperan que las secuencias de comandos se analicen y ejecuten de inmediato cuando el analizador alcanza una etiqueta <script>. Se detiene el análisis del documento hasta que se ejecuta la secuencia de comandos. Si la secuencia de comandos es externa, primero se debe recuperar el recurso de la red. Esto también se hace de forma síncrona, y el análisis se detiene hasta que se recupera el recurso. Este fue el modelo por muchos años y también se especifica en las especificaciones HTML4 y 5. Los autores pueden agregar la función "diferir" a una secuencia de comandos, en cuyo caso no detendrá el análisis del documento y se ejecutará después de que se analice. HTML5 agrega una opción para marcar la secuencia de comandos como asíncrona para que otro subproceso la analice y la ejecute.

Análisis especulativo

WebKit y Firefox realizan esta optimización. Mientras se ejecutan las secuencias de comandos, otro subproceso analiza el resto del documento y averigua qué otros recursos deben cargarse desde la red y los carga. De esta manera, los recursos pueden cargarse en conexiones paralelas y se mejora la velocidad general. Nota: El analizador especulativo solo analiza referencias a recursos externos, como secuencias de comandos externas, imágenes y hojas de estilo; no modifica el árbol del DOM; esto le deja al analizador principal.

Hojas de estilo

Por otra parte, las hojas de estilo tienen un modelo diferente. Conceptualmente, parece que, como las hojas de estilo no cambian el árbol del DOM, no hay razón para esperarlas y detener el análisis del documento. Sin embargo, existe un problema con las secuencias de comandos que solicitan información de estilo durante la etapa de análisis del documento. Si el estilo aún no se carga ni analiza, la secuencia de comandos obtendrá respuestas incorrectas y, al parecer, esto causó muchos problemas. Parece ser un caso límite, pero es bastante común. Firefox bloquea todas las secuencias de comandos cuando hay una hoja de estilo que todavía se está cargando y analizando. WebKit bloquea las secuencias de comandos únicamente cuando intentan acceder a determinadas propiedades de estilo que pueden verse afectadas por las hojas de estilo descargadas.

Construcción de árbol de renderización

Mientras se construye el árbol del DOM, el navegador construye otro árbol, el árbol de representación. Este árbol contiene elementos visuales en el orden en que se mostrarán. Es la representación visual del documento. El propósito de este árbol es permitir pintar el contenido en el orden correcto.

Firefox llama a los elementos en el árbol de representación "marcos". WebKit usa el término procesador o objeto de renderización.

Un renderizador sabe cómo diseñar y pintar a sí mismo y a sus elementos secundarios.

La clase RenderObject de WebKit, la clase básica de los procesadores, tiene la siguiente definición:

class RenderObject{
  virtual void layout();
  virtual void paint(PaintInfo);
  virtual void rect repaintRect();
  Node* node;  //the DOM node
  RenderStyle* style;  // the computed style
  RenderLayer* containgLayer; //the containing z-index layer
}

Cada representador representa un área rectangular que generalmente corresponde al cuadro CSS de un nodo, como se describe en las especificaciones CSS2. Incluye información geométrica, como el ancho, la altura y la posición.

El tipo de cuadro se ve afectado por la "pantalla". valor del atributo de estilo relevante para el nodo (consulta la sección de cálculo de estilo). A continuación, se incluye un código de WebKit para decidir qué tipo de renderizador se debe crear para un nodo del DOM, según el atributo de visualización:

RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
    Document* doc = node->document();
    RenderArena* arena = doc->renderArena();
    ...
    RenderObject* o = 0;

    switch (style->display()) {
        case NONE:
            break;
        case INLINE:
            o = new (arena) RenderInline(node);
            break;
        case BLOCK:
            o = new (arena) RenderBlock(node);
            break;
        case INLINE_BLOCK:
            o = new (arena) RenderBlock(node);
            break;
        case LIST_ITEM:
            o = new (arena) RenderListItem(node);
            break;
       ...
    }

    return o;
}

El tipo de elemento también se considera: por ejemplo, los controles de formularios y las tablas tienen marcos especiales.

En WebKit, si un elemento quiere crear un procesador especial, se anulará el método createRenderer(). Los procesadores apuntan a objetos de diseño que contienen información no geométrica.

Relación del árbol de representación con el árbol del DOM

Los representadores corresponden a elementos del DOM, pero la relación no es uno a uno. No se insertarán elementos del DOM no visuales en el árbol de representación. Un ejemplo es la columna "head" . También elementos cuyo valor de visualización se asignó a "ninguno" no aparecerán en el árbol (mientras que los elementos con visibilidad "oculta" aparecerán en el árbol).

Hay elementos del DOM que corresponden a varios objetos visuales. Por lo general, son elementos con una estructura compleja que no se puede describir con un solo rectángulo. Por ejemplo, la opción "seleccionar" tiene tres procesadores: uno para el área de visualización, uno para el cuadro de lista desplegable y otro para el botón. Además, cuando se divide el texto en varias líneas porque el ancho no es suficiente para una línea, las líneas nuevas se agregarán como procesadores adicionales.

Otro ejemplo de varios procesadores es el HTML roto. Según las especificaciones del CSS, un elemento intercalado debe contener solo elementos bloqueados o solo elementos intercalados. En el caso del contenido mixto, se crearán renderizadores de bloques anónimos para unir los elementos intercalados.

Algunos objetos de representación corresponden a un nodo del DOM, pero no en el mismo lugar en el árbol. Los elementos flotantes y de posición absoluta están fuera del flujo, ubicados en una parte diferente del árbol y asignados al marco real. Un marco de marcador de posición es donde deberían haber estado.

El árbol de representación y el árbol del DOM correspondiente
Figura 13: Árbol de representación y árbol del DOM correspondiente. La "Ventana gráfica" es el bloque contenedor inicial. En WebKit, será la "RenderView". objeto

El flujo de la construcción del árbol

En Firefox, la presentación se registra como un objeto de escucha de actualizaciones del DOM. La presentación delega la creación del marco al FrameConstructor, y el constructor resuelve el estilo (consulta el computación del estilo) y crea un marco.

En WebKit, el proceso de resolver el estilo y crear un renderizador se denomina "archivo adjunto". Cada nodo del DOM tiene un vínculo . El adjunto es síncrono, la inserción de nodos en el árbol del DOM llama al nuevo nodo "attach" .

El procesamiento de las etiquetas html y body da como resultado la construcción de la raíz del árbol de renderización. El objeto de renderización de raíz corresponde a lo que las especificaciones de CSS llaman al bloque contenedor: el bloque superior que contiene todos los demás bloques. Sus dimensiones son el viewport: las dimensiones del área de visualización de la ventana del navegador. Firefox lo llama ViewPortFrame y WebKit lo llama RenderView. Este es el objeto de renderización al que apunta el documento. El resto del árbol se construye como una inserción de nodos del DOM.

Consulta las especificaciones de CSS2 sobre el modelo de procesamiento.

Cálculo de estilo

Para compilar el árbol de renderización, es necesario calcular las propiedades visuales de cada objeto de renderización. Para ello, se calculan las propiedades de estilo de cada elemento.

El estilo incluye hojas de estilo de varios orígenes, elementos de estilo intercalados y propiedades visuales en el HTML (como la propiedad "bgcolor").Esta última se traduce a propiedades de estilo CSS coincidentes.

El origen de las hojas de estilo son las hojas de estilo predeterminadas del navegador, las hojas de estilo proporcionadas por el autor de la página y las hojas de estilo del usuario. Estas son las hojas de estilo proporcionadas por el usuario del navegador (los navegadores te permiten definir tus estilos favoritos. En Firefox, por ejemplo, se debe colocar una hoja de estilo en el "Perfil de Firefox". carpeta).

El cálculo del estilo presenta algunas dificultades:

  1. Los datos de estilo son una construcción muy grande que contiene las numerosas propiedades de estilo, lo que puede causar problemas de memoria.
  2. Encontrar las reglas de coincidencia para cada elemento puede causar problemas de rendimiento si no está optimizado. Recorrer toda la lista de reglas de cada elemento para encontrar coincidencias es una tarea difícil. Los selectores pueden tener una estructura compleja que puede hacer que el proceso de coincidencia se inicie en una ruta aparentemente prometedora que se haya demostrado inútil y que se deba intentar otra ruta.

    Por ejemplo, este selector compuesto:

    div div div div{
    ...
    }
    

    Significa que las reglas se aplican a un elemento <div> que es subordinado de 3 div. Supongamos que deseas verificar si la regla se aplica a un elemento <div> determinado. Se elige una ruta determinada hacia arriba del árbol para verificarla. Es posible que debas atravesar el árbol de nodos solo para descubrir que solo hay dos divs y que la regla no se aplica. Luego, debes probar otras rutas en el árbol.

  3. Aplicar las reglas implica reglas en cascada bastante complejas que definen la jerarquía de las reglas.

Veamos cómo los navegadores enfrentan estos problemas:

Cómo compartir datos de estilo

Los nodos de WebKit hacen referencia a objetos de estilo (RenderStyle). Los nodos pueden compartir estos objetos en determinadas condiciones. Los nodos son hermanos o primos y:

  1. Los elementos deben estar en el mismo estado del mouse (p. ej., uno no puede estar en :hover mientras el otro no).
  2. Ninguno de los elementos debe tener un ID
  3. Los nombres de la etiqueta deben coincidir
  4. Los atributos de la clase deben coincidir
  5. El conjunto de atributos asignados debe ser idéntico
  6. Los estados del vínculo deben coincidir
  7. Los estados del enfoque deben coincidir
  8. Ninguno de los elementos debería verse afectado por los selectores de atributos, en los que afectado se define como una coincidencia de selector que usa un selector de atributos en cualquier posición dentro del selector.
  9. No debe haber ningún atributo de estilo intercalado en los elementos
  10. No debe haber ningún selector del mismo nivel en uso. WebCore simplemente lanza un interruptor global cuando se encuentra un selector del mismo nivel e inhabilita el uso compartido de estilos para todo el documento cuando está presente. Esto incluye el selector + y los selectores como :first-child y :last-child.

Árbol de reglas de Firefox

Firefox tiene dos árboles adicionales para facilitar el cálculo del estilo: el árbol de reglas y el árbol de contexto de estilo. WebKit también tiene objetos de estilo, pero no se almacenan en un árbol como el árbol de contexto de estilo; solo el nodo del DOM señala su estilo correspondiente.

Árbol de contexto de estilo de Firefox.
Figura 14: Árbol de contexto de estilo Firefox.

Los contextos de estilo contienen valores finales. Los valores se calculan aplicando todas las reglas de coincidencia en el orden correcto y realizando manipulaciones que los transformen de valores lógicos a concretos. Por ejemplo, si el valor lógico es un porcentaje de la pantalla, se calculará y transformará en unidades absolutas. La idea del árbol de reglas es realmente inteligente. Permite compartir estos valores entre nodos para evitar calcularlos nuevamente. Esto también ahorra espacio.

Todas las reglas coincidentes se almacenan en un árbol. Los nodos inferiores de una ruta de acceso tienen mayor prioridad. El árbol contiene todas las rutas de acceso para las coincidencias de reglas encontradas. El almacenamiento de las reglas se realiza de forma diferida. El árbol no se calcula al principio para cada nodo, sino que cada vez que se debe calcular un estilo de nodo, las rutas calculadas se agregan al árbol.

La idea es ver las rutas de los árboles como palabras en un léxico. Digamos que ya calculamos este árbol de reglas:

Árbol de reglas calculado
Figura 15: Árbol de reglas calculado.

Supongamos que debemos hacer coincidir las reglas para otro elemento del árbol de contenido y descubrir que las reglas coincidentes (en el orden correcto) son B-E-I. Ya tenemos esta ruta de acceso en el árbol porque ya calculamos la ruta A-B-E-I-L. Ahora tendremos menos trabajo por hacer.

Veamos cómo el árbol nos ahorra trabajo.

División en structs

Los contextos de diseño se dividen en structs. Estos structs contienen información de estilo para una categoría determinada, como borde o color. Todas las propiedades de una struct son heredadas o no heredadas. Las propiedades heredadas son aquellas que, a menos que las defina el elemento, se heredan de su superior. Las propiedades no heredadas (llamadas propiedades "reset") usan valores predeterminados si no están definidos.

El árbol nos ayuda a almacenar en caché structs completas (que contienen los valores finales calculados) en el árbol. La idea es que, si el nodo inferior no proporciona una definición de struct, se puede usar un struct almacenado en caché en un nodo superior.

Cómo calcular los contextos de estilo con el árbol de reglas

Al calcular el contexto de estilo de un elemento determinado, primero calculamos una ruta en el árbol de reglas o usamos una existente. Luego, comenzamos a aplicar las reglas en la ruta de acceso para completar los structs en nuestro nuevo contexto de estilo. Comenzamos en el nodo inferior de la ruta de acceso, el que tiene la prioridad más alta (por lo general, el selector más específico) y atravesamos el árbol hasta que nuestro struct esté completo. Si no hay una especificación para el struct en ese nodo de regla, entonces podemos optimizarlo de manera considerable. Subimos el árbol hasta encontrar un nodo que lo especifique por completo y que lo señale. Esa es la mejor optimización: se comparte todo el struct. Esto ahorra el cálculo de los valores finales y la memoria.

Si encontramos definiciones parciales, subimos el árbol hasta que la struct está rellenada.

Si no encontramos ninguna definición para nuestro struct, entonces, en caso de que el struct sea un “heredado”. apuntamos a la struct de nuestro elemento superior en el árbol de contexto. En este caso, también logramos compartir las structs correctamente. Si se trata de una struct restablecida, se usarán los valores predeterminados.

Si el nodo más específico agrega valores, tendremos que hacer algunos cálculos adicionales para transformarlo en valores reales. Luego, almacenamos en caché el resultado en el nodo de árbol para que los elementos secundarios lo puedan usar.

En caso de que un elemento tenga un elemento del mismo nivel o un hermano que apunte al mismo nodo del árbol, se puede compartir el contexto del estilo completo entre ellos.

Veamos un ejemplo: Supongamos que tenemos este código HTML

<html>
  <body>
    <div class="err" id="div1">
      <p>
        this is a <span class="big"> big error </span>
        this is also a
        <span class="big"> very  big  error</span> error
      </p>
    </div>
    <div class="err" id="div2">another error</div>
  </body>
</html>

Y las siguientes reglas:

div {margin: 5px; color:black}
.err {color:red}
.big {margin-top:3px}
div span {margin-bottom:4px}
#div1 {color:blue}
#div2 {color:green}

Para simplificar, digamos que necesitamos completar solo dos structs: el struct de color y el struct de los márgenes. El struct de color contiene solo un miembro: el color El struct de los márgenes contiene los cuatro lados.

El árbol de reglas resultante se verá de la siguiente manera (los nodos están marcados con el nombre del nodo, es decir, el número de la regla a la que apuntan):

El árbol de reglas
Figura 16: El árbol de reglas

El árbol de contexto se verá de la siguiente manera (nombre del nodo: nodo de la regla al que apuntan):

El árbol de contexto.
Figura 17: El árbol de contexto

Supongamos que analizamos el HTML y llegamos a la segunda etiqueta <div>. Debemos crear un contexto de diseño para este nodo y completar sus structs de estilo.

Haremos coincidir las reglas y descubriremos que las reglas de coincidencia para <div> son 1, 2 y 6. Esto significa que ya existe una ruta de acceso en el árbol que nuestro elemento puede usar y solo debemos agregarle otro nodo para la regla 6 (nodo F en el árbol de reglas).

Crearemos un contexto de estilo y lo colocaremos en el árbol de contexto. El nuevo contexto del estilo apuntará al nodo F en el árbol de reglas.

Ahora debemos completar los structs de estilo. Comenzaremos por completar el struct de los márgenes. Dado que el último nodo de la regla (F) no se suma a la struct del margen, podemos subir por el árbol hasta encontrar una struct almacenada en caché que se calculó en una inserción de nodo anterior y usarla. Lo encontraremos en el nodo B, que es el nodo superior que especificó reglas de márgenes.

Tenemos una definición para la struct de color, por lo que no podemos usar una struct almacenada en caché. Dado que el color tiene un atributo, no es necesario subir por el árbol para rellenar otros atributos. Calcularemos el valor final (convertir la cadena en RGB, etc.) y almacenaremos en caché el struct calculado en este nodo.

El trabajo en el segundo elemento <span> es aún más fácil. Haremos coincidir las reglas y llegaremos a la conclusión de que apunta a la regla G, como el intervalo anterior. Dado que tenemos elementos del mismo nivel que apuntan al mismo nodo, podemos compartir todo el contexto del estilo y solo apuntar al contexto del intervalo anterior.

En el caso de las structs que contienen reglas heredadas de la superior, el almacenamiento en caché se realiza en el árbol de contexto (la propiedad de color en realidad se hereda, pero Firefox la trata como restablecida y la almacena en caché en el árbol de reglas).

Por ejemplo, si agregamos reglas para las fuentes en un párrafo:

p {font-family: Verdana; font size: 10px; font-weight: bold}

Entonces, el elemento de párrafo, que es un elemento secundario de div en el árbol de contexto, podría haber compartido el mismo struct de fuente que su elemento superior. Esto ocurre si no se especificaron reglas de fuente para el párrafo.

En WebKit, que no tiene un árbol de reglas, las declaraciones coincidentes se recorren cuatro veces. Las primeras propiedades de prioridad alta no importantes se aplican (las que se deben aplicar primero porque otros dependen de ellas, como la visualización), luego, las de prioridad alta, las de prioridad normal no importantes y las de prioridad normal. Esto significa que las propiedades que aparecen varias veces se resolverán según el orden de cascada correcto. Los últimos ganadores

En resumen, compartir los objetos de estilo (todos o algunos de los structs que contienen) resuelve los problemas 1 y 3. El árbol de reglas de Firefox también ayuda a aplicar las propiedades en el orden correcto.

Manipulación de las reglas para establecer coincidencias fácilmente

Existen varias fuentes de reglas de estilo:

  1. Reglas de CSS, ya sea en las hojas de estilo externas o en los elementos de estilo css p {color: blue}
  2. Los atributos de estilo intercalado, html <p style="color: blue" />
  3. Atributos visuales HTML (asignados a reglas de estilo relevantes) html <p bgcolor="blue" /> Los dos últimos pueden coincidir fácilmente con el elemento, ya que es propietario de los atributos de estilo y los atributos HTML se pueden asignar usando el elemento como clave.

Como se mencionó anteriormente en el problema n.o 2, la coincidencia de reglas de CSS puede ser más complicada. Para resolver este problema, las reglas se manipulan para que el acceso sea más sencillo.

Después de analizar la hoja de estilo, las reglas se agregan a uno de varios mapas hash, según el selector. Hay mapas por ID, por nombre de clase, por nombre de etiqueta y un mapa general para todo lo que no encaje en esas categorías. Si el selector es un ID, la regla se agregará al mapa de ID; si es una clase, se agregará al mapa de clases, etcétera.

Esta manipulación hace que sea mucho más fácil hacer coincidir las reglas. No es necesario buscar en todas las declaraciones: podemos extraer las reglas relevantes para un elemento de los mapas. Esta optimización elimina más del 95% de las reglas, por lo que ni siquiera es necesario considerarlas durante el proceso de segmentación(4.1).

Veamos, por ejemplo, las siguientes reglas de estilo:

p.error {color: red}
#messageDiv {height: 50px}
div {margin: 5px}

La primera regla se insertará en el mapa de clases. La segunda en el mapa de ID y la tercera en el mapa de etiquetas.

Para el siguiente fragmento HTML:

<p class="error">an error occurred</p>
<div id=" messageDiv">this is a message</div>

Primero, intentaremos encontrar reglas para el elemento p. El mapa de clases contendrá un “error” clave bajo la que se encuentra la regla de “p.error” si detecta posibles problemas. El elemento div tendrá reglas relevantes en el mapa de ID (la clave es el id) y en el mapa de etiquetas. Por lo tanto, lo único que falta es descubrir con cuáles de las reglas extraídas por las claves coinciden realmente.

Por ejemplo, si la regla para el elemento div es la siguiente:

table div {margin: 5px}

Se extraerá del mapa de etiquetas de todos modos, porque la clave es el selector del extremo derecho, pero no coincidiría con nuestro elemento div, que no tiene un principal de la tabla.

Tanto WebKit como Firefox realizan esta manipulación.

Orden en cascada de la hoja de estilo

El objeto de estilo tiene propiedades que corresponden a cada atributo visual (todos los atributos CSS, excepto los más genéricos). Si la propiedad no está definida por ninguna de las reglas coincidentes, el objeto de diseño de elemento superior puede heredar algunas propiedades. Otras propiedades tienen valores predeterminados.

El problema comienza cuando hay más de una definición. Aquí viene el orden en cascada para resolver el problema.

Una declaración de una propiedad de estilo puede aparecer en varias hojas de estilo y varias veces dentro de una hoja de estilo. Esto significa que el orden de aplicación de las reglas es muy importante. Esto se llama "cascada" en el orden personalizado. Según las especificaciones de CSS2, el orden en cascada es (de menor a mayor):

  1. Declaraciones del navegador
  2. Declaraciones normales del usuario
  3. Declaraciones normales de autores
  4. Declaraciones importantes del autor
  5. Declaraciones importantes de los usuarios

Las declaraciones del navegador son menos importantes y el usuario anula el autor solo si la declaración se marcó como importante. Las declaraciones con el mismo orden se ordenarán por specificity y, luego, en el orden en que se especifican. Los atributos visuales HTML se traducen a declaraciones de CSS coincidentes . Se tratan como reglas de autor con baja prioridad.

Especificidad

La especificación de CSS2 define la especificidad del selector de la siguiente manera:

  1. Contar 1 si la declaración de la cual proviene es un “estilo” en lugar de una regla con un selector; de lo contrario, 0 (= a)
  2. contar la cantidad de atributos de ID en el selector (= b)
  3. contar el número de otros atributos y seudoclases en el selector (= c)
  4. contar el número de nombres de elementos y seudoelementos en el selector (= d)

La concatenación de los cuatro números a-b-c-d (en un sistema numérico con una base grande) da la especificidad.

La base numérica que debes usar se define por el recuento más alto que tengas en una de las categorías.

Por ejemplo, si a=14 puedes usar la base hexadecimal. En el caso improbable de que a=17 necesite una base numérica de 17 dígitos. La situación posterior puede suceder con un selector como el siguiente: html body div div p... (17 etiquetas en tu selector... no es muy probable).

Estos son algunos ejemplos:

 *             {}  /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
 li            {}  /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
 li:first-line {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul li         {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul ol+li      {}  /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
 h1 + *[rel=up]{}  /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
 ul ol li.red  {}  /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
 li.red.level  {}  /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
 #x34y         {}  /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
 style=""          /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */

Ordenar las reglas

Después de que las reglas coinciden, se ordenan según las reglas de cascada. WebKit usa la ordenación con burbujas para las listas pequeñas y la ordenación combinada para las listas más grandes. WebKit implementa el ordenamiento anulando el operador > para las reglas:

static bool operator >(CSSRuleData& r1, CSSRuleData& r2)
{
    int spec1 = r1.selector()->specificity();
    int spec2 = r2.selector()->specificity();
    return (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2;
}

Proceso gradual

WebKit utiliza una función experimental que marca si se cargaron todas las hojas de estilo de nivel superior (incluidas las funciones @imports). Si el estilo no está completamente cargado al adjuntarlo, se usan marcadores de posición que se marcan en el documento y se vuelven a calcular una vez que se hayan cargado las hojas de estilo.

Diseño

Cuando se crea el procesador y se agrega al árbol, este no tiene una posición ni un tamaño. El cálculo de estos valores se denomina diseño o reprocesamiento.

HTML usa un modelo de diseño basado en flujo, lo que significa que, la mayor parte del tiempo, es posible calcular la geometría en un solo pase. Elementos más adelante “en el flujo” normalmente no afectan la geometría de los elementos anteriores "en el flujo", por lo que el diseño puede avanzar de izquierda a derecha, de arriba hacia abajo en el documento. Hay excepciones: por ejemplo, las tablas HTML pueden requerir más de un pase.

El sistema de coordenadas es relativo al marco raíz. Se usan las coordenadas superior e izquierda.

El diseño es un proceso recursivo. Comienza en el procesador raíz, que corresponde al elemento <html> del documento HTML. El diseño continúa de manera recursiva a través de una parte o la totalidad de la jerarquía de fotogramas y calcula la información geométrica para cada procesador que lo requiera.

La posición del procesador de raíz es 0,0 y sus dimensiones son el viewport (la parte visible de la ventana del navegador).

Todos los procesadores tienen un “diseño” o "reprocesamiento" cada procesador invoca el método de diseño de sus elementos secundarios que necesitan diseño.

Sistema de bit sucio

Para no crear un diseño completo para cada pequeño cambio, los navegadores usan un archivo sucio en un sistema de archivos. Un procesador que se cambia o se agrega se marca a sí mismo y a sus elementos secundarios como "sucios", lo que requiere diseño.

Hay dos banderas: "sucio" y "los niños están sucios". lo que significa que, si bien el renderizador en sí puede estar bien, tiene al menos un elemento secundario que necesita un diseño.

Diseño incremental y global

El diseño se puede activar en todo el árbol de representación; esto es "global". . Esto puede suceder como resultado de lo siguiente:

  1. Es un cambio de estilo global que afecta a todos los procesadores, como un cambio en el tamaño de fuente.
  2. Cuando se cambió el tamaño de una pantalla

El diseño puede ser incremental; solo se distribuirán los procesadores sucios (esto puede causar algún daño que requerirá diseños adicionales).

El diseño incremental se activa (de forma asíncrona) cuando los renderizadores están sucios. Por ejemplo, cuando se agregan nuevos representadores al árbol de representación después de que el contenido adicional proviene de la red y se agrega al árbol del DOM.

Diseño incremental.
Figura 18: Diseño incremental; solo se presentan procesadores sucios y sus elementos secundarios

Diseño asíncrono y síncrono

El diseño incremental se realiza de forma asíncrona. Firefox pone en cola los “comandos de reprocesamiento” para diseños incrementales y un programador activa la ejecución por lotes de estos comandos. WebKit también cuenta con un cronómetro que ejecuta un diseño incremental: el árbol se recorre y está “sucio”. los renderizadores de diseño.

Secuencias de comandos que solicitan información de estilo, como "offsetHeight" puede activar el diseño incremental de forma síncrona.

El diseño global generalmente se activa de forma síncrona.

A veces, el diseño se activa como una devolución de llamada después de un diseño inicial porque cambiaron algunos atributos, como la posición de desplazamiento.

Optimizaciones

Cuando se activa un diseño mediante un "cambio de tamaño" o un cambio en la posición del renderizador(y no en el tamaño), los tamaños de renderizaciones se toman de una caché y no se vuelven a calcular.

En algunos casos, solo se modifica un subárbol y el diseño no comienza desde la raíz. Esto puede suceder cuando el cambio es local y no afecta su entorno, como el texto que se inserta en campos de texto (de lo contrario, cada combinación de teclas activaría un diseño a partir de la raíz).

El proceso de diseño

El diseño generalmente tiene el siguiente patrón:

  1. El procesador superior determina su propio ancho.
  2. La madre o el padre habla sobre el hijo o la hija, y:
    1. Coloca el renderizador secundario (establece su x e y).
    2. Llama al diseño secundario si es necesario (están sucios, usamos un diseño global o por algún otro motivo) que calcula la altura del elemento secundario.
  3. El elemento superior usa las alturas acumuladas de los elementos secundarios, así como las alturas de los márgenes y el padding para establecer su propia altura. El elemento superior del procesador superior usará esta altura.
  4. Configura su parte sucia como falsa.

Firefox utiliza un "estado" object(nsHTMLReflowState) como parámetro del diseño (denominado "reprocesamiento"). Entre otros, el estado incluye el ancho de los elementos superiores.

El resultado del diseño de Firefox es una “métricas” objeto(nsHTMLReflowMetrics). Incluirá la altura calculada del renderizador.

Cálculo del ancho

El ancho del procesador se calcula con el ancho del bloque del contenedor y el "ancho" del estilo del renderizador propiedad, los márgenes y los bordes.

Por ejemplo, el ancho del siguiente elemento div:

<div style="width: 30%"/>

WebKit lo calcularía de la siguiente manera(clase RenderBox del método calcWidth):

  • El ancho del contenedor es el máximo de los contenedores disponiblesWidth y 0. El valor availableWidth en este caso es el contentWidth, que se calcula de la siguiente manera:
clientWidth() - paddingLeft() - paddingRight()

clientWidth y clientHeight representan el interior de un objeto. excluyendo el borde y la barra de desplazamiento.

  • El ancho de los elementos es el “ancho”. style. Se calculará como un valor absoluto calculando el porcentaje del ancho del contenedor.

  • Ahora se agregaron los bordes horizontales y el padding.

Hasta ahora, este fue el cálculo del “ancho preferido”. Ahora se calcularán los anchos mínimo y máximo.

Si el ancho preferido es mayor que el ancho máximo, se usa el ancho máximo. Si es menor que el ancho mínimo (la unidad más pequeña que no se puede romper), se usa el ancho mínimo.

Los valores se almacenan en caché en caso de que se necesite un diseño, pero el ancho no cambia.

Saltos de línea

Cuando un renderizador en medio de un diseño decide que debe fallar, el renderizador se detiene y propaga al elemento superior del diseño que debe dañarse. El elemento superior crea los procesadores adicionales y el diseño de llamadas en ellos.

Pintura

En la etapa de pintura, se recorre el árbol de representación y se utiliza la función "Paint()" del renderizador. para mostrar contenido en la pantalla. La pintura usa el componente de infraestructura de la IU.

Incremental y global

Al igual que el diseño, la pintura también puede ser global (el árbol completo está pintado) o incremental. En la pintura incremental, algunos de los renderizadores cambian de una manera que no afecta a todo el árbol. El procesador modificado invalida su rectángulo en la pantalla. Esto hace que el SO lo vea como una "región no sincronizada" y generaremos una pintura para cada evento. El SO lo hace de manera inteligente y une varias regiones en una. En Chrome es más complicado porque el procesador se encuentra en un proceso diferente del proceso principal. Chrome simula el comportamiento del SO hasta cierto punto. La presentación escucha estos eventos y delega el mensaje a la raíz de renderización. El árbol se recorre hasta que se alcanza el renderizador relevante. Volverá a pintar a sí mismo (y, por lo general, a sus hijos).

El orden de pintura

CSS2 define el orden del proceso de pintura. Este es, en realidad, el orden en que se apilan los elementos en los contextos de pila. Este orden afecta a la pintura, ya que las pilas se pintan de atrás hacia adelante. El orden de apilado de un procesador de bloques es el siguiente:

  1. background color
  2. imagen de fondo
  3. borde
  4. niños
  5. descripción

Lista de visualización de Firefox

Firefox revisa el árbol de renderización y crea una lista de visualización para el rectángulo pintado. Contiene los procesadores relevantes para el rectángulo, en el orden de pintura correcto (fondos de los renderizadores, luego bordes, etc.).

De esa manera, debes atravesar el árbol solo una vez para volver a pintar, en lugar de varias veces: pintando todos los fondos, luego todas las imágenes, después todos los bordes, etc.

Firefox optimiza el proceso al no agregar elementos que se ocultarán, como elementos completamente debajo de otros elementos opacos.

Almacenamiento rectangular de WebKit

Antes de volver a pintar, WebKit guarda el rectángulo anterior como un mapa de bits. Luego, solo pinta el delta entre los rectángulos nuevo y antiguo.

Cambios dinámicos

Los navegadores intentan realizar las acciones mínimas posibles en respuesta a un cambio. Por lo tanto, los cambios en el color de un elemento solo harán que se vuelva a pintar el elemento. Los cambios en la posición del elemento provocarán el diseño y el rediseño del elemento, sus elementos secundarios y, posiblemente, los elementos del mismo nivel. Si agregas un nodo del DOM, se creará y se volverá a renderizar el nodo. Cambios importantes, como aumentar el tamaño de la fuente "html" hará que se invaliden las cachés y que se modifique el diseño y se vuelva a renderizar el árbol completo.

Los subprocesos del motor de renderización

El motor de renderización tiene un solo subproceso. Casi todo, excepto las operaciones de red, ocurre en un solo subproceso. En Firefox y Safari, este es el subproceso principal del navegador. En Chrome, es el subproceso principal del proceso de pestañas.

Varios subprocesos paralelos pueden realizar operaciones de red. La cantidad de conexiones paralelas es limitada (por lo general, de 2 a 6 conexiones).

Bucle de evento

El subproceso principal del navegador es un bucle de eventos. Se trata de un bucle infinito que mantiene activo el proceso. Espera eventos (como eventos de diseño y pintura) y los procesa. Este es el código de Firefox para el bucle de eventos principales:

while (!mExiting)
    NS_ProcessNextEvent(thread);

Modelo visual CSS2

El lienzo

Según la especificación de CSS2, El término lienzo describe "el espacio donde se renderiza la estructura de formato": donde el navegador pinta el contenido.

El lienzo es infinito para cada dimensión del espacio, pero los navegadores eligen un ancho inicial en función de las dimensiones del viewport.

Según www.w3.org/TR/CSS2/zindex.html, El lienzo es transparente si está contenido dentro de otro y, de lo contrario, se le da un color definido por el navegador.

Modelo de cuadro de CSS

El modelo de cuadro CSS describe los cuadros rectangulares que se generan para los elementos del árbol de documentos y que se disponen según el modelo de formato visual.

Cada cuadro tiene un área de contenido (p.ej., texto, una imagen, etc.) y áreas opcionales de relleno, borde y margen.

Modelo de caja CSS2
Figura 19: Modelo de cuadro CSS2

Cada nodo genera 0...n esos cuadros.

Todos los elementos tienen una "pantalla" que determina el tipo de cuadro que se generará.

Ejemplos:

block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.

El valor predeterminado es "intercalado", pero la hoja de estilo del navegador puede establecer otros valores predeterminados. Por ejemplo, la opción de visualización predeterminada para "div". es el bloque.

Aquí encontrarás un ejemplo de una hoja de estilo predeterminada: www.w3.org/TR/CSS2/sample.html.

Esquema de posicionamiento

Existen tres esquemas:

  1. Normal: El objeto se posiciona según su lugar en el documento. Esto significa que su lugar en el árbol de representación es como su lugar en el árbol del DOM y se presenta de acuerdo con el tipo de cuadro y las dimensiones.
  2. Flotante: El objeto se establece primero como el flujo normal y, luego, se mueve lo más posible hacia la izquierda o la derecha.
  3. Absoluto: El objeto se coloca en el árbol de representación, en un lugar diferente al del árbol del DOM.

El esquema de posicionamiento se establece mediante el valor de "position" propiedad y el "flotante" .

  • estática y relativa causan un flujo normal
  • causa absoluta y fija (posicionamiento absoluto)

En el posicionamiento estático, no se define ninguna posición y se usa el posicionamiento predeterminado. En los otros esquemas, el autor especifica la posición: superior, inferior, izquierda, derecha.

La disposición de la caja se determina de la siguiente manera:

  • Tipo de cuadro
  • Dimensiones de la caja
  • Esquema de posicionamiento
  • Información externa, como el tamaño de la imagen y el tamaño de la pantalla

Tipos de caja

Cuadro de bloques: Forma un bloque que tiene su propio rectángulo en la ventana del navegador.

Cuadro de bloqueo.
Figura 20: Cuadro de bloques

Cuadro intercalado: No tiene su propio bloque, pero está dentro de uno que lo contiene.

Cuadros intercalados.
Figura 21: Cuadros intercalados

Los bloques se formatean verticalmente uno tras otro. Las líneas intercaladas tienen un formato horizontal.

Formato de bloqueo y formato intercalado
Figura 22: Formato intercalado y bloqueado

Los cuadros intercalados se colocan dentro de líneas o "cuadros de líneas". Las líneas son al menos tan altas como el cuadro más alto, pero pueden ser más altas cuando los cuadros están alineados con el "modelo de referencia". es decir, la parte inferior de un elemento se alinea en un punto de otro cuadro que no es la parte inferior. Si el ancho del contenedor no es suficiente, las líneas intercaladas se pondrán en varias líneas. Esto suele ser lo que sucede en un párrafo.

Líneas.
Figura 23: Líneas

Posicionamiento

Relativo

Posicionamiento relativo: Se posiciona como de costumbre y, luego, se mueve según el delta requerido.

Posicionamiento relativo:
Figura 24: Posicionamiento relativo

Anuncio flotante

Un cuadro flotante se desplaza a la izquierda o a la derecha de una línea. Lo interesante es que las otras cajas fluyen a su alrededor. El código HTML:

<p>
  <img style="float: right" src="images/image.gif" width="100" height="100">
  Lorem ipsum dolor sit amet, consectetuer...
</p>

Se verá de la siguiente manera:

Flotante.
Figura 25: Número de punto flotante

Absoluto y fijo

El diseño se define con exactitud, independientemente del flujo normal. El elemento no participa en el flujo normal. Las dimensiones son relativas al contenedor. En este caso, el contenedor es el viewport.

Posicionamiento fijo.
Figura 26: Posicionamiento fijo

Representación en capas

Esto se especifica mediante la propiedad CSS del índice z. Representa la tercera dimensión del cuadro: su posición a lo largo del eje "z".

Las cajas se dividen en pilas (llamadas contextos de apilamiento). En cada pila, los elementos de la parte posterior se pintan primero y los elementos posteriores se pintan en la parte superior, más cerca del usuario. En caso de superposición, el elemento que se encuentra más arriba ocultará al anterior.

Las pilas se ordenan de acuerdo con la propiedad del índice z. Cuadros con "índice z" forman una pila local. El viewport tiene la pila externa.

Ejemplo:

<style type="text/css">
  div {
    position: absolute;
    left: 2in;
    top: 2in;
  }
</style>

<p>
  <div
    style="z-index: 3;background-color:red; width: 1in; height: 1in; ">
  </div>
  <div
    style="z-index: 1;background-color:green;width: 2in; height: 2in;">
  </div>
</p>

El resultado será el siguiente:

Posicionamiento fijo.
Figura 27: Posicionamiento fijo

Aunque el div rojo precede al verde en el lenguaje de marcado y ya se habría pintado antes en el flujo regular, la propiedad del índice z es más alta, de manera que queda más adelantada en la pila que sostiene el cuadro raíz.

Recursos

  1. Arquitectura del navegador

    1. Grosskurth, de Alan. Una arquitectura de referencia para navegadores web (pdf)
    2. Gupta, Vineet. Cómo funcionan los navegadores - Parte 1 - Arquitectura
  2. Análisis

    1. Aho, Sethi, Ullman, Compilers: Principles, Techniques and Tools (también conocido como el "libro del dragón"), Addison-Wesley, 1986
    2. Rick Jelliffe The Bold and the Beautiful: dos nuevos borradores para HTML 5
  3. Firefox

    1. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers.
    2. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers (video de charla sobre tecnología de Google).
    3. L. David Baron, Motor de diseño de Mozilla
    4. L. David Baron, Documentación del sistema de estilo de Mozilla
    5. Chris Waterson, Notes on HTML Reflow (Notas sobre el reprocesamiento de HTML)
    6. Chris Waterson, Descripción general de Gecko
    7. Alexander Larsson, The Life of an HTML HTTP request.
  4. WebKit

    1. David Hyatt, Implementación de CSS(parte 1)
    2. David Hyatt, Descripción general de WebCore.
    3. David Hyatt, WebCore Rendering
    4. David Hyatt, El problema FOUC
  5. Especificaciones del W3C

    1. Especificación de HTML 4.01
    2. Especificación W3C HTML5
    3. Especificación de la revisión 1 de las Hojas de estilo en cascada de nivel 2 (CSS 2.1)
  6. Instrucciones de compilación de los navegadores

    1. Firefox. https://developer.mozilla.org/Build_Documentation
    2. WebKit http://webkit.org/building/build.html

Traducciones

Esta página se tradujo al japonés dos veces:

Puedes ver las traducciones alojadas externamente de Coreano y Turco.

¡Gracias a todos!