Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Let's build a Virtual DOM 🌈!
Search
Sponsored
·
SiteGround - Reliable hosting with speed, security, and support you can count on.
→
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
50
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
源内ハンズオン概要編
hideg
0
190
テーブルをDELETEした
yuzneri
0
150
「つくるAI」だけではバグは見つからない ~テストに必要な「見つけるAI」を分離させる戦略~
mfunaki
0
120
自動化したのに回らない テスト運用の壁―AI時代の品質責任と生産性
mfunaki
0
350
React本体のコードリーディング
high_g_engineer
1
150
実装をデザインガイドラインに追従させるための取り組み / 260731-dip-mosh-design-system
dachi023
0
6.3k
Laravel Boostに学ぶ、AIにPHPを書かせる技術 〜OSSの実装から蒸留するエージェント制御の王道〜
kentaroutakeda
3
740
jsmini JavaScript Engine を作ってみた話
yosuke_furukawa
PRO
0
340
AIと壁打ちしながら進めるコスト管理
fufuhu
0
730
AWS DevOps AgentのAzure接続機能を検証して見えた活用法/Use Cases Verified for the AWS DevOps Agent's Azure Connectivity Feature
masakiokuda
1
250
in-process GraphQL のすすめ #ginzajs
izumin5210
4
1.4k
自動化したのに回らないテスト運用の壁ーAI時代の品質責任と生産性
mfunaki
1
140
Featured
See All Featured
Leveraging Curiosity to Care for An Aging Population
cassininazir
1
470
GraphQLの誤解/rethinking-graphql
sonatard
75
12k
Navigating Team Friction
lara
192
16k
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
47
8.3k
職位にかかわらず全員がリーダーシップを発揮するチーム作り / Building a team where everyone can demonstrate leadership regardless of position
madoxten
64
56k
Gemini Prompt Engineering: Practical Techniques for Tangible AI Outcomes
mfonobong
2
490
Rails Girls Zürich Keynote
gr2m
96
14k
How STYLIGHT went responsive
nonsquared
100
6.2k
The Web Performance Landscape in 2024 [PerfNow 2024]
tammyeverts
12
1.3k
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
PRO
201
75k
Ethics towards AI in product and experience design
skipperchong
2
350
Avoiding the “Bad Training, Faster” Trap in the Age of AI
tmiket
0
210
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