This article explains the Vue reactivity system through a simplified custom implementation. It covers four core classes: Subscriber, TemplateCompiler, Dependency, and ReactiveObserver, following the publish-subscribe pattern.
Core Classes
- Subscriber: Observes data changes and executes callbacks.
- TemplateCompiler: Parses the DOM, handles interpolation expressions, and replaces data.
- Dependency: Collects dependencies and notifies updates.
- ReactiveObserver: Makes data reactive by traversing object keys and defining reactive properties via
defineReactiveProperty.
Publish-Subscribe Pattern

class Subscriber {
constructor(vm, key, callback) {
this.vm = vm;
this.key = key;
this.callback = callback;
Dependency.target = this;
this.oldValue = vm[key];
Dependency.target = null;
}
update() {
const newValue = this.vm[this.key];
if (newValue === this.oldValue) return;
this.callback(newValue);
this.oldValue = newValue;
}
}
class TemplateCompiler {
constructor(vm) {
this.vm = vm;
this.el = vm.$el;
this.compile(this.el);
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
if (node.nodeType === 3) {
this.compileText(node);
}
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
});
}
compileText(node) {
const reg = /\{\{(.+?)\}\}/g;
const value = node.textContent.replace(/\s/g, '');
const tokens = [];
let result, index, lastIndex = 0;
while ((result = reg.exec(value)) !== null) {
index = result.index;
if (index > lastIndex) {
tokens.push(value.slice(lastIndex, index));
}
const key = result[1].trim();
tokens.push(this.vm[key]);
lastIndex = index + result[0].length;
const pos = tokens.length - 1;
new Subscriber(this.vm, key, newValue => {
tokens[pos] = newValue;
node.textContent = tokens.join('');
});
}
if (lastIndex < value.length) {
tokens.push(value.slice(lastIndex));
}
if (tokens.length) {
node.textContent = tokens.join('');
}
}
}
class Dependency {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
class ReactiveObserver {
constructor(data) {
this.data = data;
this.dep = new Dependency();
this.walk(data);
}
walk(data) {
const dep = this.dep;
Object.keys(data).forEach(key => defineReactiveProperty(data, key, data[key], dep));
}
}
function defineReactiveProperty(data, key, value, dep) {
if (typeof value === 'object' && value !== null) {
return new ReactiveObserver(value);
}
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
if (Dependency.target) {
dep.addSub(Dependency.target);
}
return value;
},
set(newValue) {
if (value === newValue) return;
value = newValue;
if (typeof value === 'object' && value !== null) {
new ReactiveObserver(value);
}
dep.notify();
}
});
}
class Vue {
constructor(options) {
this.$options = options || {};
this.$data = options.data || {};
const el = options.el;
this.$el = typeof el === 'string' ? document.querySelector(el) : el;
proxy(this, this.$data);
new ReactiveObserver(this.$data);
new TemplateCompiler(this);
}
}
function proxy(target, data) {
Object.keys(data).forEach(key => {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get() {
return data[key];
},
set(newValue) {
data[key] = newValue;
}
});
});
}
Usage Example
<div id="app">
{{mytitle}} {{mytitle}}
<h2>{{nihao}}</h2>
</div>
<script src="./vue.js"></script>
<script>
let vue = new Vue({
el: '#app',
data: {
mytitle: "Hello",
nihao: "World"
}
});
console.log(vue);
</script>
Overall Flowchart
