Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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
<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
You can’t perform that action at this time.