Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Let's build a Virtual DOM 🌈!
Search
keyserfaty
November 25, 2017
Programming
62
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Let's build a Virtual DOM 🌈!
keyserfaty
November 25, 2017
More Decks by keyserfaty
See All by keyserfaty
Cómo validar una idea en 8 horas y con (casi) $0
keyserfaty
0
54
Animaciones en Vanilla JS
keyserfaty
0
51
CSS & React - Problemas, ejemplos y soluciones
keyserfaty
0
110
How to (maybe) structure a React application
keyserfaty
2
280
Other Decks in Programming
See All in Programming
ゲームコントローラやキーボードのファームウェアをSwiftで書く
kishikawakatsumi
1
250
App Storeの外へ──日本のiOSサイドローディング入門 for iOSDC Japan 2026
yuukiw00w
0
240
wkhtmltopdfの次どうするか問題2026
willnet
2
1.6k
Snowflakeで業務アプリを作ろう。 Snowflakeのアプリ機能解説&実践ガイド
ayumu_yamaguchi
2
300
『寄り添うラジオ』をAIで作る 体験価値から逆算した、会話しないUXと品質設計
theoriatec2024
3
190
Intent as Code
shoppingjaws
6
1.1k
Workers Cache を知る
syumai
0
210
WebRTC映像をAirPlayに対応させる挑戦.pdf
monolithic_adam
0
300
AHC070解法紹介
eijirou
0
130
スマートフォンでモールス信号を送受信する 〜スマートフォンのLEDとカメラで作る光通信の設計と実装〜
atsuki_seo
0
180
モジュールの視点からSwiftを読み解く #iosdc
s_shimotori
0
190
Kiroで創り、AgentCoreで繋ぐ!AWSで実践する「AI-DLC」から「AIエージェント統合」までの最新地図
licux
4
780
Featured
See All Featured
How To Stay Up To Date on Web Technology
chriscoyier
790
250k
Bridging the Design Gap: How Collaborative Modelling removes blockers to flow between stakeholders and teams @FastFlow conf
baasie
0
700
Rails Girls Zürich Keynote
gr2m
96
14k
The MySQL Ecosystem @ GitHub 2015
samlambert
251
13k
Abbi's Birthday
coloredviolet
4
10k
Heart Work Chapter 1 - Part 1
lfama
PRO
10
36k
A Guide to Academic Writing Using Generative AI - A Workshop
ks91
PRO
1
480
It's Worth the Effort
3n
188
29k
Everyday Curiosity
cassininazir
0
320
Are puppies a ranking factor?
jonoalderson
2
3.9k
How to train your dragon (web standard)
notwaldorf
97
6.8k
From π to Pie charts
rasagy
1
380
Transcript
Let’s build a Virtual DOM! JSDayUY ~ 2017
Hola! Soy Karen " Developer en Wingu (@desdewingu) => Trabajando
en Nilus (@nilusorg) @keyserfaty
Alguna vez hicieron una app sin usar ningún framework? ✋
Y sin usar jQuery tampoco? ✋
Digamos que queremos hacer que un input cambie el texto
en un div
None
Cómo hacemos esto en vanilla JS?
// Crea los nodos const $input = document.createElement('input') $input.setAttribute('type', 'text')
$input.setAttribute('value', '') const $textContainer = document.createElement('div') $textContainer.setAttribute('class', 'text-container') const $container = document.createElement('div') $container.setAttribute('class', 'container') $container.appendChild($input) $container.appendChild($textContainer) const $root = document.querySelector('#root') $root.appendChild($container)
// Appendea eventos al input $input.addEventListener('keyup', function (e) { const
text = e.target.value const $textNode = document.createTextNode(text) const $oldTextNode = $textContainer.childNodes[0] if (!$oldTextNode) { $textContainer.appendChild($textNode) } else { $textContainer.replaceChild($textNode, $oldTextNode) } })
Qué acaba de pasar?
(the real DOM)
// Appendea eventos al input $input.addEventListener('keyup', function (e) { const
text = e.target.value const $textNode = document.createTextNode(text) const $oldTextNode = $textContainer.childNodes[0] if (!$oldTextNode) { $textContainer.appendChild($textNode) } else { $textContainer.replaceChild($textNode, $oldTextNode) } })
// Appendea eventos al input $input.addEventListener('keyup', function (e) { const
text = e.target.value const $textNode = document.createTextNode(text) const $oldTextNode = $textContainer.childNodes[0] if (!$oldTextNode) { $textContainer.appendChild($textNode) } else { $textContainer.replaceChild($textNode, $oldTextNode) } })
// Appendea eventos al input $input.addEventListener('keyup', function (e) { const
text = e.target.value const $textNode = document.createTextNode(text) const $oldTextNode = $textContainer.childNodes[0] if (!$oldTextNode) { $textContainer.appendChild($textNode) } else { $textContainer.replaceChild($textNode, $oldTextNode) } })
// Appendea eventos al input $input.addEventListener('keyup', function (e) { const
text = e.target.value const $textNode = document.createTextNode(text) const $oldTextNode = $textContainer.childNodes[0] if (!$oldTextNode) { $textContainer.appendChild($textNode) } else { $textContainer.replaceChild($textNode, $oldTextNode) } })
// Appendea eventos al input $input.addEventListener('keyup', function (e) { const
text = e.target.value const $textNode = document.createTextNode(text) const $oldTextNode = $textContainer.childNodes[0] if (!$oldTextNode) { $textContainer.appendChild($textNode) } else { $textContainer.replaceChild($textNode, $oldTextNode) } })
// Appendea eventos al input $input.addEventListener('keyup', function (e) { const
text = e.target.value const $textNode = document.createTextNode(text) const $oldTextNode = $textContainer.childNodes[0] if (!$oldTextNode) { $textContainer.appendChild($textNode) } else { $textContainer.replaceChild($textNode, $oldTextNode) } })
Qué está pasando por atrás mientras hacemos todo esto?
(WebKit)
Y qué problema tiene esto?
1. El proceso de repainting del browser es lento
2. Si tenemos 200 nodos podríamos tener potencialmente 200 re-renders
Sería mejor si pudiéramos hacer todos los cambios en el
DOM en grupo en vez de individualmente
Digamos que queremos hacer nuestra ““app”” de nuevo pero usando
la API del DOM lo menos posible
Podemos empezar por no crear todos los nodos al principio
Podemos probar creando una representación de esos nodos y después
crearlos todos juntos al final
const container = { type: 'div', props: { class: 'container'
}, children: [ // input & text-container ] }
Como crear estos objetos es una experiencia horrible podemos crear
una función que los cree por nosotros
const h = (type, props, ...children) => ({ type, props,
children })
const element = ( h('div', { className: 'container' }, h('input',
{ type: 'text', onKeyUp: () => {} }), h('div', { className: 'text-container' }, ‘Un texto!’) ) )
const element = ( h('div', { className: 'container' }, h('input',
{ type: 'text', onKeyUp: () => {} }), h('div', { className: 'text-container' }, ‘Un texto!’) ) ) => const element = ( { type: 'div', props: { className: 'container' }, children: [ { type: 'input', props: { type: 'text', onKeyUp: () => {} } }, { type: 'div', props: { className: 'text-container' }, children: [ ‘Un texto!’ ] } ] } )
1- Crear una representación de los nodos del DOM
Ahora necesitamos convertir a esos nodos en elementos del DOM
posta
const createElement = node => { if (typeof node ===
'string') { return document.createTextNode(node) } return document.createElement(node.type) }
None
const createElement = node => { if (typeof node ===
'string') { return document.createTextNode(node) } const $el = document.createElement(node.type) node.children .map(child => createElement(child)) .forEach(childNode => $el.appendChild(childNode)) return $el }
const createElement = node => { if (typeof node ===
'string') { return document.createTextNode(node) } const $el = document.createElement(node.type) node.children .map(child => createElement(child)) .forEach(childNode => $el.appendChild(childNode)) return $el }
Qué pasa con las props?
const createElement = node => { if (typeof node ===
'string') { return document.createTextNode(node) } const $el = document.createElement(node.type) setProps($el, node.props) setEvents($el, node.props) node.children .map(child => createElement(child)) .forEach(childNode => $el.appendChild(childNode)) return $el }
const createElement = node => { if (typeof node ===
'string') { return document.createTextNode(node) } const $el = document.createElement(node.type) setProps($el, node.props) setEvents($el, node.props) node.children .map(child => createElement(child)) .forEach(childNode => $el.appendChild(childNode)) return $el }
const setProp = ($el, name, value) => { if (isEventProp(name))
{ return } else if (name === 'className') { $el.setAttribute('class', value) } else { $el.setAttribute(name, value) } } const setProps = ($el, props) => { Object.keys(props) .forEach(prop => setProp($el, prop, props[prop])) }
const setEvent = ($el, name, value) => { $el.addEventListener(extractName(name), value)
} const setEvents = ($el, props) => { Object.keys(props) .forEach(prop => isEventProp(prop) ? setEvent($el, prop, props[prop]) : null ) }
const createElement = node => { if (typeof node ===
'string') { return document.createTextNode(node) } const $el = document.createElement(node.type) setProps($el, node.props) setEvents($el, node.props) node.children .map(child => createElement(child)) .forEach(childNode => $el.appendChild(childNode)) return $el }
const $root = document.getElementById('root') $root.appendChild(createElement(element))
2- Crear todos los nodos juntos y appendearlos al DOM
Antes de seguir vamos a hacer una suposición:
Supongamos que estamos guardando nuestro state de alguna forma (con
Redux, por ejemplo)
Si queremos que nuestra app cambie cuando hay un cambio
en el state no podemos hacer los cambios en el eventListener
$input.addEventListener('keyup', function (e) { const text = e.target.value const $textNode
= document.createTextNode(text) const $oldTextNode = $textContainer.childNodes[0] if (!$oldTextNode) { $textContainer.appendChild($textNode) } else { $textContainer.replaceChild($textNode, $oldTextNode) } })
Tenemos que hacer algo más declarativo y menos imperativo
Por qué mejor no armamos una función a la que
le pasemos nuestro árbol de nodos
Y que haga los cambios que necesita comparándolo con el
árbol anterior
Esta función va a hacer algo parecido a esto:
$input.addEventListener('keyup', function (e) { const text = e.target.value const $textNode
= document.createTextNode(text) const $oldTextNode = $textContainer.childNodes[0] if (!$oldTextNode) { $textContainer.appendChild($textNode) } else { $textContainer.replaceChild($textNode, $oldTextNode) } })
Pero va a ser genérica y va a funcionar para
cualquier cambio que se produzca en el árbol de nodos
Tenemos nuestros dos árboles de nodos:
Momento 0: No tenemos nodos en el DOM Momento 1:
Hacemos el primer rendering de los nodos
Árbol 1: Árbol 2: (nada) { type: 'div', props: {
className: 'container' }, children: [ { type: 'input', props: { type: 'text', onKeyUp: updateText } }, { type: 'div', props: { className: 'text-container' }, children: [] } ] }
CASO 1: Hay que renderizar el nodo en el DOM
const updateElement = ($parent, newNode, oldNode) => { if (!oldNode)
{ $parent.appendChild(createElement(newNode)) } }
const updateElement = ($parent, newNode, oldNode) => { if (!oldNode)
{ $parent.appendChild(createElement(newNode)) } }
Momento 0: el div está vacío Momento 1: el div
tiene un textElement adentro con el texto ‘TEXTO!’
Árbol 1: Árbol 2: { type: 'div', props: { className:
'container' }, children: [ { type: 'input', props: { type: 'text', onKeyUp: updateText } }, { type: 'div', props: { className: 'text-container' }, children: [‘TEXTO!’] } ] } { type: 'div', props: { className: 'container' }, children: [ { type: 'input', props: { type: 'text', onKeyUp: updateText } }, { type: 'div', props: { className: 'text-container' }, children: [] } ] }
CASO 2: El contenido de uno de los children cambió
y hay que actualizarlo
const updateElement = ($parent, newNode, oldNode, index = 0) =>
{ if (!oldNode) { $parent.appendChild(createElement(newNode)) } else if (nodeChanged(newNode, oldNode)) { $parent.replaceChild(createElement(newNode), $parent.childNodes[index]) } }
const updateElement = ($parent, newNode, oldNode, index = 0) =>
{ if (!oldNode) { $parent.appendChild(createElement(newNode)) } else if (nodeChanged(newNode, oldNode)) { $parent.replaceChild(createElement(newNode), $parent.childNodes[index]) } }
Momento 0: el div tiene contenido Momento 1: el div
está vacío
Árbol 1: Árbol 2: { type: 'div', props: { className:
'container' }, children: [ { type: 'input', props: { type: 'text', onKeyUp: updateText } }, { type: 'div', props: { className: 'text-container' }, children: [‘TEXTO!’] } ] } { type: 'div', props: { className: 'container' }, children: [ { type: 'input', props: { type: 'text', onKeyUp: updateText } }, { type: 'div', props: { className: 'text-container' }, children: [] } ] }
CASO 3: Uno de los children tenía un nodo que
ya no tiene más
const updateElement = ($parent, newNode, oldNode, index = 0) =>
{ if (!oldNode) { $parent.appendChild(createElement(newNode)) } else if (!newNode) { $parent.removeChild($parent.childNodes[index]) } else if (nodeChanged(newNode, oldNode)) { $parent.replaceChild(createElement(newNode), $parent.childNodes[index]) } }
const updateElement = ($parent, newNode, oldNode, index = 0) =>
{ if (!oldNode) { $parent.appendChild(createElement(newNode)) } else if (!newNode) { $parent.removeChild($parent.childNodes[index]) } else if (nodeChanged(newNode, oldNode)) { $parent.replaceChild(createElement(newNode), $parent.childNodes[index]) } }
Esto nos va a servir para un elemento pero necesitamos
que la función recorra todo el árbol
La función completa se ve así:
const updateElement = ($parent, newNode, oldNode, index = 0) =>
{ if (!oldNode) { $parent.appendChild(createElement(newNode)) } else if (!newNode) { $parent.removeChild($parent.childNodes[index]) } else if (nodeChanged(newNode, oldNode)) { $parent.replaceChild(createElement(newNode), $parent.childNodes[index]) } else if (newNode.type) { const newLength = newNode.children.length const oldLength = oldNode.childNodes.length for (let i = 0; i < newLength || i < oldLength; i++) { updateElement($parent.childNodes[index], newNode.children[i], oldNode.children[i], i) } } }
const updateElement = ($parent, newNode, oldNode, index = 0) =>
{ if (!oldNode) { $parent.appendChild(createElement(newNode)) } else if (!newNode) { $parent.removeChild($parent.childNodes[index]) } else if (nodeChanged(newNode, oldNode)) { $parent.replaceChild(createElement(newNode), $parent.childNodes[index]) } else if (newNode.type) { const newLength = newNode.children.length const oldLength = oldNode.childNodes.length for (let i = 0; i < newLength || i < oldLength; i++) { updateElement($parent.childNodes[index], newNode.children[i], oldNode.children[i], i) } } }
3- Hacer cambios en el árbol de nodos cuando se
produzca algún evento
Hasta ahora no mencionamos al virtual DOM
Ni dijimos diffing algorithm
Ni blockchain
None
Recapitulando: Nuestra función updateElement es nuestro diffing algorithm
Compara dos árboles de nodos (del virtual DOM) y actualiza
solo las partes que cambiaron
Internamente React (y otros) tienen un proceso que es más
o menos así:
Crea nodos en el vDOM => Crea los nodos en
el DOM => (Cambios / eventos) => Aplica el diffing algorithm => Hace patching en el DOM
Crea nodos en el vDOM => Crea los nodos en
el DOM => (Cambios / eventos) => Aplica el diffing algorithm => Hace patching en el DOM
Crea nodos en el vDOM => Crea los nodos en
el DOM => (Cambios / eventos) => Aplica el diffing algorithm => Hace patching en el DOM
Crea nodos en el vDOM => Crea los nodos en
el DOM => (Cambios / eventos) => Aplica el diffing algorithm => Hace patching en el DOM
Crea nodos en el vDOM => Crea los nodos en
el DOM => (Cambios / eventos) => Aplica el diffing algorithm => Hace patching en el DOM
Crea nodos en el vDOM => Crea los nodos en
el DOM => (Cambios / eventos) => Aplica el diffing algorithm => Hace patching en el DOM
None
Entonces: 1- Creamos un virtual DOM (`h`) 2- Creamos los
elementos en el DOM (`createElement`) 3- Aplicamos los cambios necesarios (`updateElement`)
Muy interesante todo Karen. A dónde puedo ir a leer
más sobre esto?
krn.sh/jsdayuy => slides / repo / links
Gracias! @keyserfaty