Core Architecture and Minimal Vue 3 Implementation
Frontend Framework Architecture & Vue 3 Motivattions
Modern frontend frameworks like Vue emphasize simplicity and a data-driven approach. By reducing direct DOM manipulation, frameworks leverage reactivity systems, declarative rendering, and virtual DOM diffing to efficiently synchronize the UI with the application state. Vue's progressive nature allows developers to adopt it incrementally, integrating state management, routing, and UI libraries as needed.
The shift to Vue 3 introduced several key architectural changes:
- Functional API over Classes: Unlike Angular or Vue 2 (which relied on class decorators), Vue 3 and React favor functions. Function signatures provide explicit inputs and outputs, drastically improving TypeScript type inference and overall code predictability.
- Composition API: This paradigm eliminates the ambiguous
thiscontext found in Options API. By explicitly declaring reactive variables and composing logic withinsetup(), code becomes more reusable, readable, and maintainable. - Improved Tree-shaking: By exposing instance methods and properties as stanadlone functions (e.g.,
import { ref } from 'vue'), bundlers can eliminate unused code more effectively. - API Simplification: Vue 3 unified and streamlined APIs. For instance,
v-modeland.syncwere merged into a singlev-modelsyntax. The render function arguments were also flattened. - Extensibility: Custom renderers (
createRenderer) allow Vue's core to operate independently of the browser DOM, enabling rendering to native mobile platforms, terminals, or canvas. - Performance via Proxy: Vue 3 replaced
Object.definePropertywithProxyfor reactivity. This solves recursive efficiency issues, removes the need for special array handling, natively supports dynamic property addition/deletion and collection types (Map,Set), and avoids the legacy API workarounds likeVue.set.
Constructing a Minimal Vue 3
1. Application Initialization & Mounting
The core of Vue initialization involves creating an application instance, locating the host element, processing the template or render function, and appending the resulting DOM.
javascript const VueLib = { buildApp(options) { return { attach(selector) { const hostEl = document.querySelector(selector) if (!options.render) { options.render = this.transpile(hostEl.innerHTML) } const renderedEl = options.render.call(options.data ? options.data() : {}) hostEl.innerHTML = '' hostEl.appendChild(renderedEl) }, transpile(markup) { return function render() { const node = document.createElement('h3') node.textContent = this.title return node } } } } }
When handling setup alongside data, Vue 3 prioritizes setup. A proxy can manage this access layer, routing property lookups and mutations to the correct source.
javascript const contextProxy = new Proxy(instance, { get(target, key) { if (target.setupState && key in target.setupState) { return Reflect.get(target.setupState, key) } return Reflect.get(target.dataState, key) }, set(target, key, value) { if (target.setupState && key in target.setupState) { return Reflect.set(target.setupState, key, value) } return Reflect.set(target.dataState, key, value) } })
To decouple the framework from the browser, platform-specific operations should be abstracted into a custom renderer factory.
javascript const VueLib = { initRenderer({ getElement, appendNode }) { return { buildApp(options) { return { attach(selector) { const hostEl = getElement(selector) // ... render logic ... appendNode(vnode, hostEl) } } } } }, buildApp(options) { const webRenderer = VueLib.initRenderer({ getElement(sel) { return document.querySelector(sel) }, appendNode(el, parent) { parent.appendChild(el) } }) return webRenderer.buildApp(options) } }
2. Reactivity System
Vue 3 employs Proxy to intercept object access and mutations. To decouple state changes from the UI update mechanism, a dependency tracking system is required.
javascript const dependencyGraph = new WeakMap() let activeEffects = []
function watchEffect(fn) { const execute = () => { activeEffects.push(execute) try { fn() } finally { activeEffects.pop() } } execute() }
function recordDep(target, key) { const currentEffect = activeEffects[activeEffects.length - 1] if (!currentEffect) return let depsMap = dependencyGraph.get(target) if (!depsMap) { depsMap = new Map() dependencyGraph.set(target, depsMap) } let depSet = depsMap.get(key) if (!depSet) { depSet = new Set() depsMap.set(key, depSet) } depSet.add(currentEffect) }
function notifyDeps(target, key) { const depsMap = dependencyGraph.get(target) if (!depsMap) return const depSet = depsMap.get(key) if (depSet) { depSet.forEach(effect => effect()) } }
function makeReactive(obj) { return new Proxy(obj, { get(target, key) { recordDep(target, key) return Reflect.get(target, key) }, set(target, key, value) { const result = Reflect.set(target, key, value) notifyDeps(target, key) return result } }) }
The application's update function is wrapped in watchEffect, ensuring DOM updates occur automatically when reactive state mutates.
javascript this.update = watchEffect(() => { const elemant = options.render.call(contextProxy) appendNode(element, hostEl) })
3. Virtual DOM & Rendering
To avoid expensive full DOM replacements during updates, a Virtual DOM represents the UI as JavaScript objects.
javascript function createVNode(tag, attrs, children) { return { tag, attrs, children } }
The compile step generates createVNode calls instead of real DOM elements. A mounting function recursively constructs the real DOM tree from these virtual nodes.
javascript function constructElement(vnode) { const el = document.createElement(vnode.tag) if (typeof vnode.children === 'string') { el.textContent = vnode.children } else if (Array.isArray(vnode.children)) { vnode.children.forEach(child => { el.appendChild(constructElement(child)) }) } vnode.el = el // Store real DOM reference for diffing return el }
4. Diffing & Patching
During an update, the new vnode tree is compared against the old one. If the component is already mounted, a patching process takes over.
javascript function syncPatch(prevVNode, nextVNode) { const el = nextVNode.el = prevVNode.el
if (prevVNode.tag === nextVNode.tag) { const oldChildren = prevVNode.children const newChildren = nextVNode.children
if (typeof oldChildren === 'string') {
if (typeof newChildren === 'string') {
if (oldChildren !== newChildren) el.textContent = newChildren
} else {
el.textContent = ''
newChildren.forEach(c => el.appendChild(constructElement(c)))
}
} else {
if (typeof newChildren === 'string') {
el.textContent = newChildren
} else {
reconcileChildren(el, oldChildren, newChildren)
}
}
} else { // Node replacement logic } }
A simplified child reconciliation algorithm compares nodes by index, updates matching pairs, and handles leftover additions or deletions.
javascript function reconcileChildren(parentEl, oldChildren, newChildren) { const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) { syncPatch(oldChildren[i], newChildren[i]) }
if (newChildren.length > oldChildren.length) { newChildren.slice(commonLength).forEach(c => { parentEl.appendChild(constructElement(c)) }) } else if (newChildren.length < oldChildren.length) { oldChildren.slice(commonLength).forEach(c => { parentEl.removeChild(c.el) }) } }