Understanding Vue.js 2's Two-Way Data Binding Mechanism and Implementation
Vue.js 2 implements two-way data binding through a reactive system composed of four core components: Observer, Compile, Watcher, and Dep.
Observer
The Observer monitors data changes using Object.defineProperty() to define getters and setters on object properties. This allows enterception of property access and modification.
function makeReactive(obj, prop, value) {
observeValue(value);
const dep = new Dependency();
Object.defineProperty(obj, prop, {
enumerable: true,
configurable: true,
get() {
if (Dependency.currentTarget) {
dep.addSubscriber(Dependency.currentTarget);
}
return value;
},
set(newValue) {
if (value === newValue) return;
value = newValue;
dep.notifySubscribers();
}
});
}
function observeValue(data) {
if (!data || typeof data !== 'object') return;
Object.keys(data).forEach(key => {
makeReactive(data, key, data[key]);
});
}
Dependency (Dep) The Dependency class manages subscribers using the publish-subcsribe pattern.
class Dependency {
constructor() {
this.subscribers = [];
}
addSubscriber(sub) {
this.subscribers.push(sub);
}
notifySubscribers() {
this.subscribers.forEach(sub => sub.update());
}
}
Dependency.currentTarget = null;
Watcher Watcehrs act as bridges between Observers and the view, updating the DOM when data changes.
class Watcher {
constructor(vm, expression, callback) {
this.vm = vm;
this.expression = expression;
this.callback = callback;
this.currentValue = this.getValue();
}
getValue() {
Dependency.currentTarget = this;
const value = this.vm.data[this.expression];
Dependency.currentTarget = null;
return value;
}
update() {
const newValue = this.vm.data[this.expression];
const oldValue = this.currentValue;
if (newValue !== oldValue) {
this.currentValue = newValue;
this.callback.call(this.vm, newValue, oldValue);
}
}
}
Compile The Compile parses template directives, binds data to the DOM, and sets up watchers for dynamic updates.
class Compiler {
constructor(element, vm) {
this.vm = vm;
this.element = element;
this.compileNode(this.element);
}
compileNode(node) {
if (node.nodeType === 3) { // Text node
const regex = /\{\{(.*?)\}\}/;
const match = node.textContent.match(regex);
if (match) {
this.compileText(node, match[1]);
}
} else if (node.nodeType === 1) { // Element node
this.compileAttributes(node);
}
if (node.childNodes && node.childNodes.length) {
Array.from(node.childNodes).forEach(child => this.compileNode(child));
}
}
compileText(node, key) {
const initialValue = this.vm[key];
node.textContent = initialValue !== undefined ? initialValue : '';
new Watcher(this.vm, key, value => {
node.textContent = value !== undefined ? value : '';
});
}
compileAttributes(node) {
Array.from(node.attributes).forEach(attr => {
if (attr.name.startsWith('v-')) {
const directive = attr.name.substring(2);
const expression = attr.value;
if (directive === 'model') {
this.bindModel(node, expression);
} else if (directive.startsWith('on:')) {
this.bindEvent(node, expression, directive.substring(3));
}
node.removeAttribute(attr.name);
}
});
}
bindModel(node, key) {
node.value = this.vm[key];
new Watcher(this.vm, key, value => {
node.value = value !== undefined ? value : '';
});
node.addEventListener('input', event => {
this.vm[key] = event.target.value;
});
}
bindEvent(node, methodName, eventType) {
node.addEventListener(eventType, () => {
this.vm.methods[methodName].call(this.vm);
});
}
}
Integrating Components A Vue-like class integrates these components to enable two-way binding.
class MiniVue {
constructor(options) {
this.data = options.data;
this.methods = options.methods || {};
this.proxyData();
observeValue(this.data);
new Compiler(document.querySelector(options.el), this);
if (options.mounted) options.mounted.call(this);
}
proxyData() {
Object.keys(this.data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return this.data[key];
},
set(value) {
this.data[key] = value;
}
});
});
}
}
Example usage:
<div id="app">
<h2>{{title}}</h2>
<input v-model="name">
<h1>{{name}}</h1>
<button v-on:click="changeTitle">Click</button>
</div>
<script>
new MiniVue({
el: '#app',
data: {
title: 'Initial Title',
name: 'User'
},
methods: {
changeTitle() {
this.title = 'Updated Title';
}
},
mounted() {
setTimeout(() => {
this.name = 'New Name';
}, 2000);
}
});
</script>