Created
June 27, 2020 07:02
-
-
Save asafmor/b6177294deee8fe3d6749178c4df5612 to your computer and use it in GitHub Desktop.
mini-vue, UI framework foundation, including concepts like: reactivity, rendering, virtual dom
This file contains 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> | |
.green { | |
color: green; | |
} | |
</style> | |
<div id="app"></div> | |
<script> | |
// vdom | |
function h(tag, props, children) { | |
return { | |
tag, | |
props, | |
children | |
} | |
} | |
function mount(vnode, container) { | |
const el = vnode.el = document.createElement(vnode.tag) | |
// props | |
if (vnode.props) { | |
for (const key in vnode.props) { | |
const value = vnode.props[key] | |
if (key.startsWith('on')) { | |
el.addEventListener(key.slice(2).toLowerCase(), value) | |
} else { | |
el.setAttribute(key, value) | |
} | |
} | |
} | |
// children | |
if (vnode.children) { | |
if (typeof vnode.children === 'string') { | |
el.textContent = vnode.children | |
} else { | |
vnode.children.forEach(child => { | |
mount(child, el) | |
}) | |
} | |
} | |
container.appendChild(el) | |
} | |
function patch(n1, n2) { | |
if (n1.tag === n2.tag) { | |
const el = n2.el = n1.el | |
//props | |
const oldProps = n1.props || {} | |
const newProps = n2.props || {} | |
for (const key in newProps) { | |
const oldValue = oldProps[key] | |
const newValue = newProps[key] | |
if (newValue !== oldValue) { | |
el.setAttribute(key, newValue) | |
} | |
} | |
for (const key in oldProps) { | |
if (!(key in newProps)) { | |
el.removeAttribute(key) | |
} | |
} | |
// children | |
const oldChildren = n1.children | |
const newChildren = n2.children | |
if (typeof newChildren === 'string') { | |
if (typeof oldChildren === 'string') { | |
if (newChildren !== oldChildren) { | |
el.textContent = newChildren | |
} | |
} else { | |
el.textContent = newChildren | |
} | |
} else { | |
if (typeof oldChildren === 'string') { | |
el.innerHTML = '' | |
newChildren.forEach(child => { | |
mount(child, el) | |
}) | |
} else { | |
const commonLength = Math.min(oldChildren.length, newChildren.length) | |
for (let i = 0; i < commonLength; i++) { | |
patch(oldChildren[i], newChildren[i]) | |
} | |
if (newChildren.length > oldChildren.length) { | |
newChildren.slice(oldChildren.length).forEach(child => { | |
mount(child, el) | |
}) | |
} else if (newChildren.length < oldChildren.length) { | |
oldChildren.slice(newChildren.length).forEach(child => { | |
el.removeChild(child.el) | |
}) | |
} | |
} | |
} | |
} else { | |
//replace | |
} | |
} | |
// reactivity | |
let activeEffect | |
class Dep { | |
subscribers = new Set() | |
depend() { | |
if (activeEffect) { | |
this.subscribers.add(activeEffect) | |
} | |
} | |
notify() { | |
this.subscribers.forEach(effect => { | |
effect() | |
}) | |
} | |
} | |
const targetMap = new WeakMap() | |
function getDep(target, key) { | |
let depsMap = targetMap.get(target) | |
if (!depsMap) { | |
depsMap = new Map() | |
targetMap.set(target, depsMap) | |
} | |
let dep = depsMap.get(key) | |
if (!dep) { | |
dep = new Dep() | |
depsMap.set(key, dep) | |
} | |
return dep | |
} | |
const reactiveHandlers = { | |
get(target, key, receiver) { | |
const dep = getDep(target, key) | |
dep.depend() | |
return Reflect.get(...arguments) | |
}, | |
set(target, key, value, receiver) { | |
const dep = getDep(target, key) | |
const result = Reflect.set(...arguments) | |
dep.notify() | |
return result | |
} | |
} | |
function watchEffect(effect) { | |
activeEffect = effect | |
effect() | |
activeEffect = null | |
} | |
function reactive(raw) { | |
return new Proxy(raw, reactiveHandlers) | |
} | |
const App = { | |
data: reactive({ | |
count: 0 | |
}), | |
render() { | |
return h('div', { | |
onClick: () => this.data.count++ | |
}, | |
[ // children | |
h('h1', { | |
class: 'green' | |
}, String(this.data.count)) | |
]) | |
} | |
} | |
function mountApp(component, container) { | |
let isMounted = false | |
let prevVdom | |
watchEffect(() => { | |
if (!isMounted) { | |
prevVdom = component.render() | |
mount(prevVdom, container) | |
isMounted = true | |
} else { | |
newVdom = component.render() | |
patch(prevVdom, newVdom) | |
prevVdom = newVdom | |
} | |
}) | |
} | |
mountApp(App, document.getElementById('app')) | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment