Vue's source code implements dependency collection using the Observer pattern. It involves three main classes:
Dep: Acts as the observable target. Every data property has aDepinstance containing asubsqueue (short for subscribers) that holds allWatcherinstances depending on the data. When data changes,dep.notify()is called to inform all watchers.Watcher: Acts as the observer. It wraps observer functions (e.g.,render()) intoWatcherinstances.Observer: A helper class that makes arrays and objects observable by converting them.
The Observer pattern defines a one-to-many dependency between objects sothat when one object changes state, all its dependents are notified and updated automatically.
Core Concepts
Observer Pattern Overview
- Intent: Define a one-to-many dependency between objects; when one object changes state, all dependents are notified and updated.
- Problem: An object's state change needs to be communicated to other objects without tight coupling.
- Solution: Use an abstract subject that maintains a list of observers. Concrete subjects notify observers when state changes.
- Key Code: An abstract class holds an
ArrayListof observers. - Usage Examples: Auction (auctioneer notifies bidders); event handling systems.
Implementation
Below is a simplified implementation demonstrating the pattern:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mini Vue Demo</title>
</head>
<body>
<div id="app">
<div @click="getValue">
<p k-text="inputData"></p>
</div>
<p @click="getValue">Hello {{ form.test }} you {{ form.test }} {{ obj.a.c }}</p>
<p>{{ inputData }}</p>
<input :value="inputData" @input="setInput" />
</div>
<script src="./kvue.js"></script>
<script>
new Kvue({
el: 'app',
data: {
form: { test: '1124dsa' },
inputData: '',
test: 111,
obj: { a: { c: 3 } }
},
watch: {
inputData(val) { console.log(val) }
},
created() {
console.log(this.test)
console.log(this.obj)
},
methods: {
getValue() { console.log(this) },
setInput(event) { this.inputData = event.target.value }
}
})
</script>
</body>
</html>
// Helper functions
function getData(data, vm) {
return data.call(vm, vm);
}
function parsePath(path) {
const segments = path.split('.');
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return undefined;
obj = obj[segments[i]];
}
return obj;
};
}
function initMethods(vm, methods) {
for (let key in methods) {
vm[key] = typeof methods[key] !== 'function' ? function(){} : methods[key].bind(vm);
}
}
function initWatch(vm, watch) {
for (let key in watch) {
new Watcher(vm, key, watch[key]);
}
}
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/;
class Kvue {
constructor(options) {
let data = options.data;
this.$data = typeof data === 'function' ? getData(data, this) : data || {};
const keys = Object.keys(this.$data);
let i = keys.length;
while (i--) {
this.proxyData(keys[i]);
}
this.observe(this.$data);
initWatch(this, options.watch);
initMethods(this, options.methods);
new Compile(this, options.el);
if (options.created) {
options.created.call(this);
}
}
observe(obj) {
if (!obj || Object.prototype.toString.call(obj) !== '[object Object]') return;
Object.keys(obj).forEach(key => {
this.defineReactive(obj, key, obj[key]);
});
}
defineReactive(obj, key, val) {
this.observe(val); // Recursively make nested objects reactive
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if (Dep.target) dep.addSub(Dep.target);
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify();
}
});
}
proxyData(key) {
Object.defineProperty(this, key, {
get() { return this.$data[key]; },
set(newVal) { this.$data[key] = newVal; }
});
}
}
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
Dep.target = null;
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
Dep.target = this;
parsePath(this.key)(this.vm); // Access property to trigger getter and collect dependency
Dep.target = null;
this.update();
}
update() {
this.cb.call(this.vm, parsePath(this.key)(this.vm));
}
}
class Compile {
constructor(vm, el) {
this.$vm = vm;
this.$el = document.getElementById(el);
if (this.$el) {
this.$fragment = this.nodeToFragment(this.$el);
this.compile(this.$fragment);
this.$el.appendChild(this.$fragment);
}
}
nodeToFragment(el) {
const frag = document.createDocumentFragment();
let child;
while (child = el.firstChild) {
frag.appendChild(child);
}
return frag;
}
compile(frag) {
const nodes = Array.from(frag.childNodes);
nodes.forEach(node => {
if (this.isElementNode(node)) {
const attrs = Array.from(node.attributes);
attrs.forEach(attr => {
const name = attr.name;
const value = attr.value;
if (this.isDirective(name)) {
const directive = name.substring(2);
if (this[directive]) this[directive](node, this.$vm, value);
}
if (this.isEvent(name)) {
const event = name.substring(1);
this.eventHandler(node, this.$vm, event, value);
}
});
}
if (this.isTextNode(node)) {
this.textNodeHandler(node, this.$vm);
}
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
});
}
update(node, vm, exp, type) {
const updateFn = this[`update${type}`];
new Watcher(vm, exp, function(value) {
if (updateFn) updateFn(node, value);
});
}
isElementNode(node) { return node.nodeType === 1; }
isTextNode(node) { return node.nodeType === 3; }
isDirective(attrName) { return attrName.startsWith('k-'); }
isEvent(attrName) { return attrName.startsWith('@'); }
text(node, vm, exp) {
this.update(node, vm, exp, 'Text');
}
textNodeHandler(node, vm) {
const match = defaultTagRE.exec(node.textContent);
if (match) {
const exp = match[1].trim();
this.update(node, vm, exp, 'TextNode');
this.textNodeHandler(node, vm); // Handle multiple interpolations
}
}
updateText(node, value) {
node.textContent = value;
}
updateTextNode(node, value) {
const content = node.textContent;
if (content) {
node.textContent = content.replace(defaultTagRE, value);
}
}
eventHandler(node, vm, event, exp) {
const fn = vm[exp];
node.addEventListener(event, fn);
}
}
The implementation demonstrates:
- Data reactivity:
defineReactivemakes each property observable viaObject.defineProperty. - Dependency tracking:
Depcollects activeWatcherinstances during property access. - Compilation:
Compileparses templates, binds directives and events, and creates watchers for reactive updates. - Watcher: Re-evaluates expressions and triggers updates when dependencies change.
This mini Vue captures the essence of Vue's reactive system and template compilation.