Project Architecture & Build Configuration
Despite the widespread adoption of Vue 3, understanding the Vue 2 architecture remains valuable for legacy maintenance and grasping foundational rendering concepts. The repository relies on Rollup for bundling and historically used Flow for type checking (modernized to TypeScript in later releases). New projects should prioritize Vue 3 due to its cleaner module boundaries, though the underlying reactive principles remain consistent.
The source tree is organized into distinct functional zones:
/src/compiler: Template parsing and AST generation/src/core: Core runtime, reactivity engine, and instance lifecycle/src/platforms: Target-specific adapters (browser, Weex, server)/src/shared: Cross-module utilities and constants
Rollup exposes multiple build targets through environment variables defined in scripts/config.js. Common variants include:
web-runtime: Minimal runtime without template compilationweb-full: Includes compiler + runtime-cjs/-esm: Module format specificationsweb-server-renderer: Nodee.js streaming hydration
In production builds leveraging @vue/cli, the vue-loader pre-compiles Single File Components during bundling. Consequently, the runtime-only build is selected to eliminate template parsing overhead at startup.
Initialization Entry Points
The browser entry point platforms/web/entry-runtime-with-compiler.js extends the base mount behavior. It intercepts instantiation to perform template resolution: if no explicit template property exists, it scrapes the inner HTML of the target element. The parsed string is then compiled into a render function and attached to the component options before delegating to the core runtime constructor found at ./runtime/index.
Source Debugging Setup
To attach breakpoints during development, modify the dev script to generate inline source maps:
// package.json
"scripts": {
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
}
Running npm run dev launches a watcher process. Injecting debugger; statements within the source files will now pause execution correctly in Chrome DevTools or VS Code.
Reactivity Engine Implementation
Vue 2 achieves two-way data binding by intercepting property access via Object.defineProperty. Each observable property wraps a dependency tracker and a subscriber list.
// core/observer/defineReactive.js (Refactored for clarity)
export function initializeReactiveProperty(target, propertyKey, descriptor = {}) {
const dependencyTracker = new Dep();
let currentValue = target[propertyKey];
const originalGetter = descriptor.get;
const originalSetter = descriptor.set;
Object.defineProperty(target, propertyKey, {
configurable: true,
enumerable: true,
get() {
const resolvedValue = originalGetter ? originalGetter.call(target) : currentValue;
// Push active watcher to dependency stack
if (Dep.activeTarget) {
dependencyTracker.register(Dep.activeTarget);
dependencyTracker.dependNested(resolvedValue);
}
return resolvedValue;
},
set(newValue) {
if (newValue === currentValue || (Number.isNaN(newValue) && Number.isNaN(currentValue))) return;
const updatedValue = originalSetter ? originalSetter.call(target, newValue) : newValue;
currentValue = updatedValue;
dependencyTracker.broadcastUpdate();
}
});
}
The getter archives references to active watchers, while the setter triggers notifications when assignments occur. Recursive traversal ensures deeply nested objects also become reactive.
Template Compilation Pipeline
String templates undergo lexical analysis, converting markup in to an Abstract Syntax Tree (AST). Pass two transforms this AST into optimized JavaScript expressions wrapped in a with(this){} block, granting implicit access to component properties.
// Example transformation output
function compileToFunction(templateString) {
const ast = parseMarkup(templateString);
return optimizeAst(ast, generateRenderFunction);
}
// Generated render payload
with(this) {
return _createElement('div', { attrs: { id: 'app' } }, [
_createElement('p', [_createTextVnode(computedFullName + ' - ' + modelValue)])
]);
}
Internals like _createElement, _createTextVnode, and _toString map directly to Virtual DOM node factories, bypassing string concatenation overhead.
Dependency Tracking & Watcher Lifecycle
Data fetching during render initiates dependency collection. Assignment events broadcast state changes. The shceduler decouples computation from mutation to batch synchronous updates into macro-tasks.
// core/observer/dep.js (Refactored)
class DependencyGraph {
constructor() {
this.subscribers = [];
}
register(watcherInstance) {
if (!this.subscribers.includes(watcherInstance)) {
this.subscribers.push(watcherInstance);
}
}
broadcastUpdate() {
this.subscribers.forEach(w => w.scheduleExecution());
}
}
// core/observer/watcher.js (Refactored)
class ReactiveObserver {
constructor(context, expressionPath, callback, options = {}) {
this.context = context;
this.callback = callback;
this.lazyEvaluation = options.lazy || false;
this.syncImmediate = options.sync || false;
this.trackingMap = new Set();
this.value = this.lazyEvaluation ? undefined : this.evaluateState();
}
evaluateState() {
Dep.activeTarget = this;
let capturedState;
try {
capturedState = typeof this.expressionPath === 'function'
? this.expressionPath.call(this.context, this.context)
: resolvePath(this.context, this.expressionPath);
} finally {
Dep.activeTarget = null;
}
return capturedState;
}
scheduleExecution() {
if (this.lazyEvaluation) {
this.markDirty();
} else if (this.syncImmediate) {
this.executeImmediate();
} else {
Scheduler.queueTask(this);
}
}
executeImmediate() {
const previousSnapshot = this.value;
this.value = this.evaluateState();
if (this.value !== previousSnapshot || deepStructuralChange(previousSnapshot, this.value)) {
this.callback.call(this.context, this.value, previousSnapshot);
}
}
}
Watchers utilize duplicate-prevention sets to ensure subscribers aren't registered multiple times per property change. The scheduler groups rapid mutations, flushing the queue asynchronously unless overridden by .sync modifiers.
Virtual DOM Rendering & Patching Strategy
Component updates route through updateComponent(), which invokes vm._render() to produce a fresh VNode tree, followed by vm._update(newVnode). The update hook delegates to the platform-specific patch() function.
// core/vdom/patch.js excerpt
export function mountOrRecycle(prevTree, nextTree, rootContainer, hydrationContext) {
if (!prevTree) {
rootContainer.appendChild(createDOMFromVNode(nextTree));
return nextTree.el;
}
return reconcileTrees(prevTree, nextTree, rootContainer);
}
The reconciliation algorithm employs an O(n) heuristic known as the four-pointer diff strategy. It compares child arrays from both ends simultaneously:
- Head-to-Head: Match
newStartwitholdStart. Advance both forward. - Tail-to-Tail: Match
newEndwitholdEnd. Retreat both backward. - Tail-to-Head: Match
newEndwitholdStart. Move matched DOM element to front. - Head-to-Tail: Match
newStartwitholdEnd. Move matched DOM element to back.
If none of these directional matches succeed, the algorithm falls back to building an index map using key attributes. Unknown nodes trigger full insertion or removal operations. This approach optimizes common array mutations like push(), pop(), and unshifts, minimizing expensive direct DOM manipulations while preserving component state across transitions.