├── 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 |
14 |
15 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/reactivity.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 🧡
8 |
9 |
10 |
11 |
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 |
--------------------------------------------------------------------------------