Created
May 23, 2020 12:24
-
-
Save themarcba/4975abf6340fd3816172c77d4cb8c3ef to your computer and use it in GitHub Desktop.
This is the click counter example for a blog post I wrote about creating your own mini-Vue.js. https://marc.dev/blog/vue-from-scratch-part-5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<style> | |
* { | |
user-select: none; | |
} | |
body { | |
margin: 0; | |
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; | |
} | |
#app { | |
height: 100vh; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
background: #41b883; | |
color: #ffffff; | |
} | |
h1 { | |
font-size: 10rem; | |
font-weight: 900; | |
margin: 0; | |
} | |
p { | |
margin: 0; | |
text-align: center; | |
font-weight: 100; | |
font-size: 3rem; | |
} | |
</style> | |
<div id="app" onclick="state.count++"></div> | |
<script> | |
// ------------------------------------------------------ | |
// VDOM | |
// ------------------------------------------------------ | |
// Create virtual node | |
function h(tag, props, children) { | |
// Return the virtual node | |
return { | |
tag, | |
props, | |
children, | |
} | |
} | |
// Mount a virtual node to the DOM | |
function mount(vnode, container) { | |
// Create the element | |
const el = (vnode.el = document.createElement(vnode.tag)) | |
// Set properties | |
for (const key in vnode.props) { | |
el.setAttribute(key, vnode.props[key]) | |
} | |
// Handle children | |
if (typeof vnode.children === 'string') { | |
el.textContent = vnode.children | |
} else { | |
vnode.children.forEach((child) => { | |
mount(child, el) | |
}) | |
} | |
// Mount to the DOM | |
container.appendChild(el) | |
} | |
// Unmount a virtual node from the DOM | |
function unmount(vnode) { | |
vnode.el.parentNode.removeChild(vnode.el) | |
} | |
// Take 2 virtual nodes, compare & figure out what's the difference | |
function patch(n1, n2) { | |
const el = (n2.el = n1.el) | |
// Case where the nodes are of different tags | |
if (n1.tag !== n2.tag) { | |
mount(n2, el.parentNode) | |
unmount(n1) | |
} | |
// Case where the nodes are of the same tag | |
else { | |
// New virtual node has string children | |
if (typeof n2.children === 'string') { | |
el.textContent = n2.children | |
} | |
// New virtual node has array children | |
else { | |
// Old virtual node has string children | |
if (typeof n1.children === 'string') { | |
el.textContent = '' | |
n2.children.forEach((child) => mount(child, el)) | |
} | |
// Case where the new vnode has string children | |
else { | |
const c1 = n1.children | |
const c2 = n2.children | |
const commonLength = Math.min(c1.length, c2.length) | |
// Patch the children both nodes have in common | |
for (let i = 0; i < commonLength; i++) { | |
patch(c1[i], c2[i]) | |
} | |
// Old children was longer | |
// Remove the children that are not "there" anymore | |
if (c1.length > c2.length) { | |
c1.slice(c2.length).forEach((child) => { | |
unmount(child) | |
}) | |
} | |
// Old children was shorter | |
// Add the newly added children | |
else if (c2.length > c1.length) { | |
c2.slice(c1.length).forEach((child) => { | |
mount(child, el) | |
}) | |
} | |
} | |
} | |
} | |
} | |
let activeEffect | |
class Dep { | |
subscribers = new Set() | |
depend() { | |
if (activeEffect) this.subscribers.add(activeEffect) | |
} | |
notify() { | |
this.subscribers.forEach((sub) => sub()) | |
} | |
} | |
function watchEffect(fn) { | |
const wrappedFn = () => { | |
activeEffect = wrappedFn | |
// clean up the deps | |
fn() | |
activeEffect = null | |
} | |
wrappedFn() | |
} | |
function reactive(obj) { | |
Object.keys(obj).forEach((key) => { | |
const dep = new Dep() | |
let value = obj[key] | |
Object.defineProperty(obj, key, { | |
get() { | |
dep.depend() | |
return value | |
}, | |
set(newValue) { | |
if (newValue !== value) { | |
value = newValue | |
dep.notify() | |
} | |
}, | |
}) | |
}) | |
return obj | |
} | |
function render(clickCount) { | |
return h('div', { class: 'container' }, [ | |
h('h1', null, clickCount), | |
h('p', null, 'clicks'), | |
]) | |
} | |
const state = reactive({ | |
count: 0, | |
}) | |
let previousVnode | |
watchEffect(() => { | |
if (!previousVnode) { | |
previousVnode = render(String(state.count)) | |
mount(previousVnode, document.getElementById('app')) | |
} else { | |
const newVnode = render(String(state.count)) | |
patch(previousVnode, newVnode) | |
previousVnode = newVnode | |
} | |
}) | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment