├── README.md ├── index.html ├── reactivity.html ├── tailwind.js ├── vdom-examples.js ├── vue-vdom-extended.js └── vue-vdom.js /README.md: -------------------------------------------------------------------------------- 1 | # Simplified Vue 3 Virtual DOM 2 | This is an example showing how a virtual DOM could be implemented. This is not a complete implementation for a VDOM, rather than an example for **educational purposes**. 3 | 4 | The [vue-vdom.js](vue-vdom.js) has a very simplified implementation of the `patch` function to keep the example simple. 5 | 6 | [vue-vdom-extended.js](vue-vdom-extended.js) has a bit more elaborate algorithm. 7 | 8 | In the file [vdom-examples.js](vdom-examples.js) are various different examples of render functions. -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 🧡 9 | 10 | 11 |
12 |
13 |
14 | 15 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /reactivity.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 🧡 8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 | 16 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /vdom-examples.js: -------------------------------------------------------------------------------- 1 | import { h, unmount, patch } from './vue-vdom.js'; 2 | 3 | // VDOM 1 --------------------------------- 4 | export const vdom1 = h( 5 | 'h1', 6 | { class: 'text-orange-500 text-3xl font-bold' }, 7 | 'Vue.js Amsterdam 🧡' 8 | ); 9 | 10 | // VDOM 2 --------------------------------- 11 | export const vdom2 = h( 12 | 'button', 13 | { 14 | class: 'bg-gray-200 p-2 rounded', 15 | onClick: () => alert('🤘'), 16 | }, 17 | 'Click here 🎉' 18 | ); 19 | 20 | // VDOM 3 --------------------------------- 21 | export const vdom3 = h( 22 | 'div', 23 | { class: 'bg-gray-800 rounded-full p-6' }, 24 | h('h1', { class: 'text-6xl' }, '🍕') 25 | ); 26 | 27 | // VDOM 4 --------------------------------- 28 | export const vdom4 = h('div', { class: 'bg-gray-800 rounded p-4' }, [ 29 | h('h1', { class: 'text-white text-2xl' }, 'Yummy foods'), 30 | h('ol', { class: 'list-decimal text-white ml-4' }, [ 31 | h('li', null, '🍕'), 32 | h('li', null, '🍔'), 33 | h('li', null, '🌮'), 34 | h('li', null, '🍟'), 35 | ]), 36 | ]); 37 | 38 | // VDOM 5 --------------------------------- 39 | const ducks = h('span', { class: 'text-3xl' }, '🦆🦆🦆'); 40 | const monkeys = h('span', { class: 'text-3xl' }, '🙈🙊🙉'); 41 | const goats = h('span', { class: 'text-3xl' }, '🐐🐐🐐'); 42 | export const vdom5 = h('div', { class: 'text-center rounded p-4' }, [ 43 | h( 44 | 'h1', 45 | { class: 'text-2xl font-bold' }, 46 | "I don't have no time for no monkey business" 47 | ), 48 | h('div', null, [ducks, monkeys, goats]), 49 | h( 50 | 'button', 51 | { 52 | class: 'text-3xl bg-gray-200 p-2 rounded mt-4', 53 | onClick: () => unmount(monkeys), 54 | }, 55 | '🚫🐒' 56 | ), 57 | ]); 58 | 59 | // VDOM 7 --------------------------------- 60 | export const vdom6 = h('div', { class: 'flex flex-col items-center' }, [ 61 | h('h1', { class: 'font-bold' }, "It's not a bug..."), 62 | h('p', { class: 'text-5xl my-4' }, '🐛'), 63 | h( 64 | 'button', 65 | { 66 | class: 'bg-black text-white p-2 rounded hover:bg-orange-500', 67 | onClick: () => patch(vdom6, vdom6_patch), 68 | }, 69 | 'Patch 🩹' 70 | ), 71 | ]); 72 | export const vdom6_patch = h('div', { class: 'flex flex-col items-center' }, [ 73 | h('h1', { class: 'font-bold' }, "... it's a feature!"), 74 | h('div', { class: 'text-5xl' }, '✨'), 75 | h('div', { class: 'text-5xl' }, '✨'), 76 | h('div', { class: 'text-5xl' }, '✨'), 77 | h('div', { class: 'text-5xl' }, '✨'), 78 | ]); 79 | -------------------------------------------------------------------------------- /vue-vdom-extended.js: -------------------------------------------------------------------------------- 1 | // Create virtual node 2 | export function h(tag, props, children) { 3 | // Return the virtual node 4 | return { 5 | tag, 6 | props, 7 | children, 8 | }; 9 | } 10 | 11 | // Mount a virtual node to the DOM 12 | export function mount(vnode, container) { 13 | // Create the element 14 | const el = document.createElement(vnode.tag); 15 | vnode.el = el; 16 | 17 | // Set properties & event listeners 18 | for (const key in vnode.props) { 19 | if (key.startsWith('on')) { 20 | // Handle event listeners 21 | // key = onClick 22 | // key.slice(2).toLowerCase = click 23 | // vnode.props[key] = function 24 | el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key]); 25 | } else { 26 | // Handle attributes 27 | el.setAttribute(key, vnode.props[key]); 28 | } 29 | } 30 | 31 | // Handle children 32 | if (typeof vnode.children === 'string') { 33 | el.textContent = vnode.children; 34 | } else { 35 | vnode.children.forEach(child => { 36 | mount(child, el); 37 | }); 38 | } 39 | 40 | // Mount to the DOM 41 | container.appendChild(el); 42 | } 43 | 44 | // Unmount a virtual node from the DOM 45 | export function unmount(vnode) { 46 | vnode.el.parentNode.removeChild(vnode.el); 47 | } 48 | 49 | // Take 2 virtual nodes, compare & figure out what's the difference 50 | export function patch(n1, n2) { 51 | const el = (n2.el = n1.el); 52 | 53 | // Case where the nodes are of different tags 54 | if (n1.tag !== n2.tag) { 55 | mount(n2, el.parentNode); 56 | unmount(n1); 57 | } 58 | 59 | // Case where the nodes are of the same tag 60 | else { 61 | // New virtual node has string children 62 | if (typeof n2.children === 'string') { 63 | el.textContent = n2.children; 64 | } 65 | 66 | // New virtual node has array children 67 | else { 68 | // Old virtual node has string children 69 | if (typeof n1.children === 'string') { 70 | el.textContent = ''; 71 | n2.children.forEach(child => mount(child, el)); 72 | } 73 | 74 | // Case where the new vnode has string children 75 | else { 76 | const c1 = n1.children; 77 | const c2 = n2.children; 78 | const commonLength = Math.min(c1.length, c2.length); 79 | 80 | // Patch the children both nodes have in common 81 | for (let i = 0; i < commonLength; i++) { 82 | patch(c1[i], c2[i]); 83 | } 84 | 85 | // Old children was longer 86 | // Remove the children that are not "there" anymore 87 | if (c1.length > c2.length) { 88 | c1.slice(c2.length).forEach(child => { 89 | unmount(child); 90 | }); 91 | } 92 | 93 | // Old children was shorter 94 | // Add the newly added children 95 | else if (c2.length > c1.length) { 96 | c2.slice(c1.length).forEach(child => { 97 | mount(child, el); 98 | }); 99 | } 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /vue-vdom.js: -------------------------------------------------------------------------------- 1 | // Create a virtual node 2 | export function h(tag, props, children) { 3 | return { tag, props, children }; 4 | } 5 | 6 | // tag: h1 7 | // props: { class: 'text-red-500'} 8 | // children: 'Hello' 9 | // Add a virtual node onto the DOM 10 | export function mount(vnode, container) { 11 | const el = document.createElement(vnode.tag); 12 | vnode.el = el; 13 | 14 | // Handle props 15 | for (const key in vnode.props) { 16 | // key: class 17 | // vnode.props[key]: 'text-red-500 18 | if (key.startsWith('on')) { 19 | // When it's an event 20 | // onClick => click 21 | const eventName = key.slice(2).toLowerCase(); 22 | el.addEventListener(eventName, vnode.props[key]); 23 | } else { 24 | // When it's a regular attribute 25 | el.setAttribute(key, vnode.props[key]); 26 | } 27 | } 28 | 29 | // Add children 30 | if (typeof vnode.children === 'string') { 31 | // Text 32 | el.textContent = vnode.children; 33 | } else if (Array.isArray(vnode.children)) { 34 | // Array of vnodes 35 | vnode.children.forEach(child => mount(child, el)); 36 | } else { 37 | // Single vnode 38 | mount(vnode.children, el); 39 | } 40 | 41 | // Add to real DOM 42 | container.appendChild(el); 43 | } 44 | 45 | // Remove a vnode from the real DOM 46 | export function unmount(vnode) { 47 | vnode.el.parentNode.removeChild(vnode.el); 48 | } 49 | 50 | // Check for differences and apply changes 51 | // (very simplified version) 52 | export function patch(vnode1, vnode2) { 53 | const el = vnode1.el; 54 | vnode2.el = el; 55 | if (typeof vnode2.children === 'string') { 56 | el.textContent = vnode2.children; 57 | } else { 58 | // Assume an array of h() 59 | for (let i = 0; i < vnode2.children.length; i++) { 60 | patch(vnode1.children[i], vnode2.children[i]); 61 | } 62 | } 63 | } 64 | --------------------------------------------------------------------------------