¿Cómo entender y dominar la esencia del DOM virtual? Recomiendo a todos que aprendan el proyecto Snabbdom.
Snabbdom es una biblioteca de implementación de DOM virtual. Los motivos de la recomendación son: primero, el código es relativamente pequeño y el código central tiene solo unos cientos de líneas; segundo, Vue se basa en las ideas de este proyecto para implementar el DOM virtual; Las ideas de diseño/implementación y expansión de este proyecto merecen su referencia.
snabb /snab/, sueco, significa rápido.
Ajuste su postura cómoda para sentarse y anímese. Para aprender DOM virtual, primero debemos conocer los conocimientos básicos de DOM y los puntos débiles de operar DOM directamente con JS.
DOM (Modelo de objetos de documento) es un modelo de objetos de documento que utiliza una estructura de árbol de objetos para representar un documento HTML/XML. El final de cada rama del árbol es un nodo. Los métodos de la API DOM le permiten manipular este árbol de formas específicas. Con estos métodos, puede cambiar la estructura, el estilo o el contenido del documento.
Todos los nodos en el árbol DOM son primero Node
Node
es una clase base. Element
, Text
y Comment
heredan de él.
En otras palabras, Element
, Text
y Comment
son tres Node
especiales, que se denominan ELEMENT_NODE
respectivamente.
TEXT_NODE
y COMMENT_NODE
representan nodos de elementos (etiquetas HTML), nodos de texto y nodos de comentarios. Element
también tiene una subclase llamada HTMLElement
. ¿Cuál es la diferencia entre HTMLElement
y Element
? HTMLElement
representa elementos en HTML, como: <span>
, <img>
, etc., y algunos elementos no son estándar HTML, como <svg>
. Puede utilizar el siguiente método para determinar si este elemento es HTMLElement
:
document.getElementById('myIMG') instanciade HTMLElement;
Es "caro" para el navegador crear el DOM. Tomemos un ejemplo clásico. Podemos crear un elemento p simple a través de document.createElement('p')
e imprimir todos los atributos:
Puede ver que hay muchos atributos impresos cuando se actualizan con frecuencia árboles DOM complejos, se producirán problemas de rendimiento. Virtual DOM utiliza un objeto JS nativo para describir un nodo DOM, por lo que crear un objeto JS es mucho menos costoso que crear un objeto DOM.
VNode es una estructura de objeto que describe el DOM virtual en Snabbdom. El contenido es el siguiente:
tipo Clave = cadena número símbolo; interfaz VNode { // Selector de CSS, como por ejemplo: 'p#container'. sel: cadena | indefinido; // Manipular clases CSS, atributos, etc. a través de módulos. datos: VNodeData | indefinido; // Matriz de nodos secundarios virtuales, los elementos de la matriz también pueden ser cadenas. hijos: Matriz<VNode | cadena> | indefinido; // Apunta al objeto DOM real creado. olmo: Nodo | indefinido; /** * Hay dos situaciones para el atributo de texto: * 1. El selector sel no está configurado, lo que indica que el nodo en sí es un nodo de texto. * 2. se establece sel, lo que indica que el contenido de este nodo es un nodo de texto. */ texto: cadena | indefinido; // Se utiliza para proporcionar un identificador para el DOM existente, que debe ser único entre los elementos hermanos para evitar eficazmente operaciones de reconstrucción innecesarias. clave: clave | indefinido; } // Algunas configuraciones en vnode.data, enlaces de funciones de clase o ciclo de vida, etc. interfaz VNodeData { ¿Accesorios?: Accesorios; atributos?: atributos; ¿clase?: Clases; ¿estilo?: VNodeStyle; ¿conjunto de datos?: conjunto de datos; encendido?: encendido; adjuntarDatos?: AdjuntarDatos; gancho?: Ganchos; clave?: Clave; ns?: cadena; // para SVG fn?: () => VNode; // para procesadores argumentos?: cualquiera[]; // para procesadores es?: cadena; // para elementos personalizados v1 [clave: cadena]: cualquiera; // para cualquier otro módulo de terceros }
Por ejemplo, defina un objeto vnode como este:
const vnode = h( 'p#contenedor', {clase: {activo: verdadero}}, [ h('span', { estilo: { fontWeight: 'bold' } }, 'Esto está en negrita'), 'y esto es sólo texto normal' ]);
Creamos objetos vnode mediante la función h(sel, b, c)
. La implementación del código h()
determina principalmente si los parámetros b y c existen y los procesa en datos y los hijos eventualmente tendrán la forma de una matriz. Finalmente, el formato de tipo VNode
definido anteriormente se devuelve a través de la función vnode()
.
Primero, tomemos un diagrama de ejemplo simple del proceso de ejecución y primero tengamos un concepto general del proceso:
El procesamiento de diferencias es el proceso utilizado para calcular la diferencia entre nodos nuevos y antiguos.
Veamos un código de muestra ejecutado por Snabbdom:
importar { inicio, módulo de clase, módulo de accesorios, módulo de estilo, módulo de escucha de eventos, h, } de 'snabbdom'; parche constante = inicio([ // Inicializa la función de parche classModule pasando el módulo, // Habilita la función de clases propsModule, // Admite el paso de accesorios styleModule, // Admite estilos en línea y animación eventListenersModule, // Agrega escucha de eventos]); // <p id="contenedor"></p> contenedor constante = document.getElementById('contenedor'); constante vnodo = h( 'p#contenedor.dos.clases', { en: { clic: algunaFn } }, [ h('span', { estilo: { fontWeight: 'bold' } }, 'Esto está en negrita'), 'y esto es sólo texto normal', h('a', { props: { href: '/foo' } }, "¡Te llevaré a lugares!"), ] ); // Pasar un nodo de elemento vacío. parche(contenedor,vnode); constante nuevoVnodo = h( 'p#contenedor.dos.clases', { en: { clic: otroEventHandler } }, [ h( 'durar', {estilo: { fontWeight: 'normal', fontStyle: 'cursiva' } }, 'Esto ahora está en cursiva' ), 'y esto sigue siendo sólo texto normal', h('a', { props: { href: ''/bar' } }, "¡Te llevaré a lugares!"), ] ); // Llame a patch() nuevamente para actualizar el nodo antiguo al nuevo nodo. patch(vnode, newVnode);
Como se puede ver en el diagrama de proceso y el código de muestra, el proceso de ejecución de Snabbdom se describe de la siguiente manera:
primero llame init()
para la inicialización y los módulos que se utilizarán deben configurarse durante la inicialización. Por ejemplo, classModule
se usa para configurar el atributo de clase de elementos en forma de objetos; el módulo eventListenersModule
se usa para configurar detectores de eventos, etc. La función patch()
se devolverá después de llamar init()
.
Cree el objeto vnode inicializado a través de la función h()
, llame a la función patch()
para actualizarlo y finalmente cree el objeto DOM real a través de createElm()
.
Cuando se requiera una actualización, cree un nuevo objeto vnode, llame patch()
para actualizar y complete la actualización diferencial de este nodo y los nodos secundarios a través de patchVnode()
y updateChildren()
.
Snabbdom utiliza el diseño de módulos para ampliar la actualización de propiedades relacionadas en lugar de escribirlo todo en el código central. Entonces, ¿cómo se diseña e implementa esto? A continuación, veamos primero el contenido principal del diseño de Kangkang, Hooks: funciones del ciclo de vida.
Snabbdom proporciona una serie de funciones de ciclo de vida enriquecidas, también conocidas como funciones de gancho. Estas funciones de ciclo de vida se pueden aplicar en módulos o se pueden definir directamente en vnode. Por ejemplo, podemos definir la ejecución del gancho en vnode de esta manera:
h('p.row', { clave: 'miFila', gancho: { insertar: (vnodo) => { console.log(vnode.elm.offsetHeight); }, }, });
todas las funciones del ciclo de vida se declaran de la siguiente manera:
nombre | del nodo activador | parámetros de devolución de llamada |
---|---|---|
pre | parche inicio ejecución | ninguno |
init | vnode se agrega | vnode |
create | un elemento DOM basado en vnode se crea | emptyVnode, vnode |
insert | elemento se inserta en el DOM | vnode |
prepatch | elemento es a punto de parchear | oldVnode, vnode |
update | de vnode se ha actualizado | oldVnode, vnode |
postpatch | de vnode ha sido parcheado | oldVnode, vnode |
destroy | de vnode se ha eliminado directa o | vnode |
remove | de vnode ha eliminado vnode del DOM | vnode, removeCallback |
post | removeCallback ha completado el proceso de parche, | ninguno |
aplicable al módulo: pre
, create
, update
, destroy
, remove
, post
. Aplicables a las declaraciones de vnode son: init
, create
, insert
, prepatch
, update
, postpatch
, destroy
, remove
.
Veamos cómo se implementa Kangkang. Por ejemplo, tomemos classModule
:
import {VNode, VNodeData} from "../vnode"; importar {Módulo} desde "./módulo"; tipo de exportación Clases = Registro<cadena, booleano>; función updateClass (oldVnode: VNode, vnode: VNode): void { // Aquí están los detalles de la actualización del atributo de clase, ignórelo por ahora. //... } export const classModule: Module = { create: updateClass, update: updateClass };
Puede ver que la última definición del módulo exportado es un objeto. La clave del objeto es el nombre de la Module
de enlace. de la siguiente manera:
importar { pregancho, crear gancho, gancho de actualización, destruir gancho, quitar gancho, gancho de correo, } de "../ganchos"; tipo de exportación Módulo = Parcial<{ pre: Preenganche; crear: CrearHook; actualización: UpdateHook; destruir: DestruirHook; eliminar: Eliminar gancho; publicación: PostHook; }>;
Partial
en TS significa que los atributos de cada clave en el objeto pueden estar vacíos, es decir, simplemente defina qué gancho le interesa en la definición del módulo. Ahora que el gancho está definido, ¿cómo se ejecuta en el proceso? A continuación, veamos la función init()
:
// ¿Cuáles son los ganchos que se pueden definir en el módulo? ganchos constantes: Matriz<clave del módulo> = [ "crear", "actualizar", "eliminar", "destruir", "pre", "correo", ]; inicio de la función de exportación ( módulos: Matriz<Partial<Módulo>>, domApi?:DOMAPI, ¿opciones?: Opciones ) { // La función de enlace definida en el módulo finalmente se almacenará aquí. const cbs: MóduloHooks = { crear: [], actualizar: [], eliminar: [], destruir: [], antes: [], correo: [], }; //... // Recorre los ganchos definidos en el módulo y guárdalos juntos. for (const gancho de ganchos) { for (módulo constante de módulos) { const currentHook = módulo[gancho]; si (currentHook! == indefinido) { (cbs[gancho] como cualquier[]).push(currentHook); } } } //... }
Puede ver que init()
primero atraviesa cada módulo durante la ejecución y luego almacena la función de enlace en el objeto cbs
. Al ejecutar, puede utilizar patch()
:
función de exportación init( módulos: Matriz<Partial<Módulo>>, domApi?:DOMAPI, ¿opciones?: Opciones ) { //... parche de función de retorno ( oldVnode: VNode | Elemento | DocumentoFragmento, vnodo: VNodo ): VNodo { //... // se inicia el parche, ejecuta el prehook. para (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); //... } }
Aquí tomamos el pre
como ejemplo. El tiempo de ejecución del pre
es cuando el parche comienza a ejecutarse. Puede ver que patch()
llama cíclicamente a los ganchos pre
relacionados almacenados en cbs
al comienzo de la ejecución. Las llamadas a otras funciones del ciclo de vida son similares a esta. Puede ver las llamadas a funciones del ciclo de vida correspondientes en otras partes del código fuente.
La idea de diseño aquí es el patrón de observador . Snabbdom implementa funciones no centrales distribuyéndolas en módulos. Combinado con la definición del ciclo de vida, el módulo puede definir los ganchos que le interesan. Luego, cuando se ejecuta init()
, se procesa en objetos cbs
para registrar estos ganchos; cuando llegue el momento de ejecución, llame. Estos enlaces se utilizan para notificar el procesamiento del módulo. Esto separa el código central y el código del módulo. Desde aquí podemos ver que el patrón de observador es un patrón común para el desacoplamiento de código.
A continuación llegamos a la función principal de Kangkang patch()
. Esta función se devuelve después de la llamada init()
. Su función es montar y actualizar el VNode. La firma es la siguiente:
function patch(oldVnode: VNode | Element. | Fragmento de documento, vnodo: VNodo): VNodo { // Por simplicidad, no prestes atención a DocumentFragment. //... }
El parámetro oldVnode
es el antiguo VNode o elemento DOM o fragmento de documento, y el parámetro vnode
es el objeto actualizado. Aquí publico directamente una descripción del proceso:
llamar al gancho pre
registrado en el módulo.
Si oldVnode
es Element
, se convierte en un objeto vnode
vacío y elm
se registra en el atributo.
El juicio aquí es si es Element
(oldVnode as any).nodeType === 1
está completo. nodeType === 1
indica que es un ELEMENT_NODE, que se define aquí.
Luego, determine si oldVnode
y vnode
son iguales. Aquí se llamará sameVnode()
para determinar:
function SameVnode(vnode1: VNode, vnode2: VNode): boolean { //Misma clave. const isSameKey = vnode1.key === vnode2.key; // Componente web, nombre de etiqueta de elemento personalizado, consulte aquí: // https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createElement const isSameIs = vnode1.data?.is === vnode2.data?.is; //Mismo selector. const esSameSel = vnode1.sel === vnode2.sel; // Los tres son iguales. return isSameSel && isSameKey && isSameIs; }
patchVnode()
para actualizar las diferencias.createElm()
para crear un nuevo nodo DOM después de la creación, inserte el nodo DOM y elimine el nodo DOM anterior.Se pueden insertar nuevos nodos llamando a la cola de enlace de insert
registrada en el objeto vnode involucrado en la operación anterior, patchVnode()
createElm()
. En cuanto a por qué se hace esto, se mencionará en createElm()
.
Finalmente, se llama al enlace post
registrado en el módulo.
El proceso consiste básicamente en hacer diferencias si los vnodes son iguales, y si son diferentes crear unos nuevos y eliminar los antiguos. A continuación, echemos un vistazo a cómo createElm()
crea nodos DOM.
createElm()
crea un nodo DOM basado en la configuración de vnode. El proceso es el siguiente:
llame al gancho init
que pueda existir en el objeto vnode.
Luego abordaremos varias situaciones:
si vnode.sel === '!'
, este es el método utilizado por Snabbdom para eliminar el nodo original, de modo que se insertará un nuevo nodo de comentario. Debido a que los nodos antiguos se eliminarán después de createElm()
, esta configuración puede lograr el propósito de la desinstalación.
Si existe la definición del selector vnode.sel
:
analice el selector y obtenga id
, tag
y class
.
Llame document.createElement()
o document.createElementNS
para crear un nodo DOM, regístrelo en vnode.elm
y configure id
, tag
y class
según los resultados del paso anterior.
Llame al gancho create
en el módulo.
Procese la matriz children
:
si children
es una matriz, llame createElm()
de forma recursiva para crear el nodo secundario y luego llame appendChild
para montarlo en vnode.elm
.
Si children
no es una matriz pero vnode.text
existe, significa que el contenido de este elemento es texto. En este momento, se llama createTextNode
para crear un nodo de texto y se monta en vnode.elm
.
Llame al gancho create
en el vnode. Y agregue el gancho insert
en vnode a la cola de ganchos insert
.
La situación restante es que vnode.sel
no existe, lo que indica que el nodo en sí es texto, luego llame createTextNode
para crear un nodo de texto y grabarlo en vnode.elm
.
Finalmente regrese vnode.elm
.
Se puede ver en todo el proceso que createElm()
elige cómo crear nodos DOM en función de diferentes configuraciones sel
. Hay un detalle que agregar aquí: la cola de enlace insert
mencionada en patch()
. La razón por la que se necesita esta cola de enlace insert
es que debe esperar hasta que el DOM se inserte realmente antes de ejecutarlo, y también debe esperar hasta que se inserten todos los nodos descendientes, para que podamos calcular la información de tamaño y posición del elemento en insert
para ser preciso. Combinado con el proceso de creación de nodos secundarios anterior, createElm()
es una llamada recursiva para crear nodos secundarios, por lo que la cola primero registrará los nodos secundarios y luego a sí misma. De esta forma se puede garantizar el orden al ejecutar la cola al final de patch()
.
A continuación, veamos cómo Snabbdom usa patchVnode()
para hacer diferencias, que es el núcleo del DOM virtual. El flujo de procesamiento de patchVnode()
es el siguiente:
primero ejecute el enlace prepatch
en vnode.
Si oldVnode y vnode tienen la misma referencia de objeto, se devolverán directamente sin procesar.
Llame a ganchos update
en módulos y vnodes.
Si no se define vnode.text
, se manejan varios casos de children
:
si oldVnode.children
y vnode.children
existen y no son iguales. Luego llame updateChildren
para actualizar.
vnode.children
existe pero oldVnode.children
no existe. Si oldVnode.text
existe, bórrelo primero y luego llame addVnodes
para agregar un nuevo vnode.children
.
vnode.children
no existe pero oldVnode.children
sí. Llame removeVnodes
para eliminar oldVnode.children
.
Si no existen oldVnode.children
ni vnode.children
. Borre oldVnode.text
si existe.
Si vnode.text
está definido y es diferente de oldVnode.text
. Si oldVnode.children
existe, llame removeVnodes
para borrarlo. Luego configure el contenido del texto a través de textContent
.
Finalmente ejecute el gancho postpatch
en el vnode.
Se puede ver en el proceso que los cambios en los atributos relacionados de sus propios nodos en diff, como class
, style
, etc., son actualizados por el módulo. Sin embargo, no lo ampliaremos demasiado aquí si es necesario. Puede echar un vistazo al código relacionado con el módulo. El procesamiento central principal de diff se centra en children
. A continuación, Kangkang diff procesa varias funciones relacionadas de children
.
es muy simple. Primero llame createElm()
para crearlo y luego insértelo en el padre correspondiente.
destory
se elimina remove
destory
, este gancho se llama primero. La lógica es llamar primero al enlace en el objeto vnode y luego llamar al enlace en el módulo. Luego, este gancho se llama recursivamente en vnode.children
en este orden.remove
, este gancho solo se activará cuando el elemento actual se elimine de su elemento principal. Los elementos secundarios en el elemento eliminado no se activarán y este gancho se llamará tanto en el módulo como en el objeto vnode. Encienda el módulo primero y luego llame a vnode. Lo que es más especial es que el elemento no se eliminará realmente hasta que se llamen todas remove
. Esto puede lograr algunos requisitos de eliminación retrasada.De lo anterior se puede ver que la lógica de llamada de estos dos ganchos es diferente. En particular, remove
solo se llamará en elementos que estén directamente separados del padre.
updateChildren()
se utiliza para procesar la diferencia de nodos secundarios y también es una función relativamente compleja en Snabbdom. La idea general es establecer un total de cuatro punteros de cabeza y cola para oldCh
y newCh
. Estos cuatro punteros son oldStartIdx
, oldEndIdx
, newStartIdx
y newEndIdx
respectivamente. Luego compare las dos matrices en while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
para encontrar las mismas partes para reutilizar y actualizar, y mueva hasta un par de punteros para cada comparación. El proceso transversal detallado se procesa en el siguiente orden:
si alguno de los cuatro punteros apunta a vnode == null, entonces el puntero se mueve al medio, como por ejemplo: start++ o end--, la aparición de null se explicará más adelante.
Si los nodos de inicio antiguos y nuevos son iguales, es decir, sameVnode(oldStartVnode, newStartVnode)
devuelve verdadero, use patchVnode()
para realizar la diferenciación y ambos nodos de inicio se moverán un paso hacia el medio.
Si los nodos finales antiguos y nuevos son iguales, también se usa patchVnode()
y los dos nodos finales retroceden un paso hacia el medio.
Si el antiguo nodo de inicio es el mismo que el nuevo nodo final, use patchVnode()
para procesar la actualización primero. Luego, es necesario mover el nodo DOM correspondiente a oldStart. La estrategia de movimiento es moverse antes del siguiente nodo hermano del nodo DOM correspondiente a oldEndVnode
. ¿Por qué se mueve así? En primer lugar, oldStart es lo mismo que newEnd, lo que significa que en el procesamiento del bucle actual, el nodo inicial de la matriz anterior se mueve hacia la derecha, porque cada procesamiento mueve los punteros de cabeza y cola hacia el medio, estamos actualizando el; matriz antigua a la nueva En este momento, es posible que oldEnd aún no se haya procesado, pero en este momento se ha determinado que oldStart es el último en el procesamiento actual de la nueva matriz, por lo que es razonable pasar al siguiente hermano. nodo de oldEnd. Una vez completado el movimiento, oldStart++ y newEnd-- mueven un paso al centro de sus respectivas matrices.
Si el antiguo nodo final es el mismo que el nuevo nodo inicial, primero se usa patchVnode()
para procesar la actualización y luego el nodo DOM correspondiente a oldEnd se mueve al nodo DOM correspondiente a oldStartVnode
. igual que el paso anterior. Una vez completado el movimiento, oldEnd--, newStart++.
Si nada de lo anterior es el caso, use la clave de newStartVnode para encontrar el subíndice idx en oldChildren
. Hay dos lógicas de procesamiento diferentes dependiendo de si el subíndice existe:
si el subíndice no existe, significa que newStartVnode se creó recientemente. Cree un nuevo DOM a través de createElm()
e insértelo antes del DOM correspondiente a oldStartVnode
.
Si el subíndice existe, se manejará en dos casos:
si el sel de los dos vnodes es diferente, aún se considerará como recién creado, cree un nuevo DOM a través de createElm()
e insértelo antes del DOM correspondiente a oldStartVnode
.
Si sel es el mismo, la actualización se procesa a través de patchVnode()
y el vnode correspondiente al subíndice de oldChildren
se establece en indefinido. Es por eso que == null aparece en el recorrido anterior del doble puntero. Luego inserte el nodo actualizado en el DOM correspondiente a oldStartVnode
.
Una vez completadas las operaciones anteriores, newStart++.
Una vez completado el recorrido, todavía quedan dos situaciones por resolver. Una es que oldCh
se ha procesado por completo, pero todavía hay nuevos nodos en newCh
y es necesario crear un nuevo DOM para cada newCh
restante. La otra es que newCh
se ha procesado por completo y todavía hay nodos antiguos en oldCh
. Es necesario eliminar los nodos redundantes. Las dos situaciones se manejan de la siguiente manera:
función updateChildren( parentElm: Nodo, oldCh: VNode[], nuevoCh: VNode[], insertadoVnodeQueue: VNodeQueue ) { // Proceso de recorrido de doble puntero. //... // Hay nuevos nodos en newCh que deben crearse. si (nuevoIdxInicio <= nuevoIdxEndx) { //Debe insertarse antes del último newEndIdx procesado. antes = newCh[newEndIdx + 1] == nulo nulo: newCh[newEndIdx + 1].elm; agregarVnodos( padreElm, antes, nuevoCh, nuevoStartIdx, nuevoEndIdx, insertadoVnodeQueue ); } // Todavía hay nodos antiguos en oldCh que deben eliminarse. si (oldStartIdx <= oldEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } }
Usemos un ejemplo práctico para observar el proceso de procesamiento de updateChildren()
:
el estado inicial es el siguiente, la antigua matriz de nodos secundarios es [A, B, C] y la nueva matriz de nodos es [B, A, C , D]:
En la primera ronda de comparación, los nodos inicial y final son diferentes, por lo que verificamos si newStartVnode existe en el nodo anterior y encontramos la posición de oldCh [1]. Luego ejecutamos patchVnode()
para actualizar primero y luego configuramos oldCh [1]. ] = indefinido e inserte el DOM antes de oldStartVnode
, newStartIdx
retrocede un paso y el estado después del procesamiento es el siguiente:
En la segunda ronda de comparación, oldStartVnode
y newStartVnode
son iguales. Cuando se ejecuta patchVnode()
para actualizar, oldStartIdx
y newStartIdx
se mueven al medio Después del procesamiento, el estado es el siguiente:
En la tercera ronda de comparación, oldStartVnode == null
, oldStartIdx
pasa al medio y el estado se actualiza de la siguiente manera:
En la cuarta ronda de comparación, oldStartVnode
y newStartVnode
son iguales. Cuando se ejecuta patchVnode()
para actualizar, oldStartIdx
y newStartIdx
se mueven al medio Después del procesamiento, el estado es el siguiente:
En este momento, oldStartIdx
es mayor que oldEndIdx
y el ciclo finaliza. En este momento, todavía hay nuevos nodos que no se han procesado en newCh
y es necesario llamar addVnodes()
para insertarlos. El estado final es el siguiente:
, el contenido principal del DOM virtual se ha resuelto aquí. Creo que los principios de diseño e implementación de Snabbdom son muy buenos, si tiene tiempo, puede ir a los detalles del código fuente de Kangkang para verlo más de cerca. Vale la pena aprender las ideas.