Vue3 S5 - Mini Vue
Vue
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 {
// ...
}
}
} else {
// replace
}
}
// reactivity
let activeEffect;
class Dep {
subscribers = new Set();
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect);
}
}
notify() {
this.subscribers.forEach((effect) => {
effect();
});
}
}
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
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(target, key, receiver);
},
set(target, key, value, receiver) {
const dep = getDep(target, key);
const result = Reflect.set(target, key, value, receiver);
dep.notify();
return result;
},
};
function reactive(raw) {
return new Proxy(raw, reactiveHandlers);
}
const App = {
data: reactive({
count: 0,
}),
render() {
return h(
'div',
{
onClick: () => this.data.count++,
},
String(this.data.count),
);
},
};
function mountApp(component, container) {
let prevVDom;
let isMounted = false;
watchEffect(() => {
if (!isMounted) {
const vdom = component.render();
mount(prevVDom, container);
isMounted = true;
} else {
const newVDom = component.render();
patch(prevVDom, newVDom);
prevVDom = newVDom;
}
});
}
mountApp(App);