MVVM là kiến trức phổ biến nhưng khá trừu tượng với nhiều lập trình viên. Bài viết này sẽ đi sâu vào cách Vue 2 triển khai mô hình này, từ lý thuyết đến việc tự xây dựng một phiên bản đơn giản.
Mục tiêu tổng quan
Chúng ta sẽ làm rõ các vấn đề sau:
- Khái niệm MVVM và cách Vue áp dụng
- Cơ chế vận hành bên trong của Vue
- Hệ thống phản ứng (Reactivity)
- Cơ chế thu thập và theo dõi dependency
- Quá trình biên dịch template
Hiểu về MVVM
MVVM (Model-View-ViewModel) là mẫu kiến trúc do Microsoft đề xuất từ 2005. Trong đó:
- Model: Dữ liệu nguồn, thường từ server
- View: Giao diện người dùng nhìn thấy
- ViewModel: Lớp trung gian, là cốt lõi của MVVM
Hai luồng chính cần thực hiện:
- Model → View: Đổi dữ liệu thành giao diện qua data binding
- View → Model: Chuyển tương tác người dùng thành thay đổi dữ liệu qua event listening
Kết hợp cả hai tạo thành two-way data binding.
Trong Vue:
- Model = các biến JS (string, number, object...)
- View = DOM
- ViewModel = instance Vue được khởi tạo
Các thành phần cốt lõi trong Vue
1. Observer
Đảm nhận việc data hijacking (đánh cắp/thay đổi dữ liệu). Vue duyệt toàn bộ data object, sử dụng Object.defineProperty để biến mỗi property thành accessor property. Khi đọc dữ liệu (getter) hoặc ghi dữ liệu (setter), các hook tương ứng sẽ kích hoạt.
2. Compiler
Chịu trách nhiệm biến template thành render function, khởi tạo giao diện ban đầu và gắn các hàm cập nhật cho những vị trí sử dụng dữ liệu.
3. Dep (Dependency)
Bộ thu thập dependency, quản lý danh sách các subscriber (Watcher). Khi getter chạy, Dep lưu Watcher hiện tại. Khi setter chạy, Dep thông báo cho toàn bộ subscriber cập nhật.
Thuộc tính Dep.target đánh dấu Watcher đang được xử lý.
4. Watcher
Đóng vai trò cầu nối giữa View và Model. Khi dữ liệu thay đổi, Watcher nhận thông báo và kích hoạt hàm cập nhật giao diện.
Luồng xử lý dữ liệu hai chiều
Chiều Model → View
- Observer chuyển data thành reactive qua
Object.defineProperty - Khi Watcher đọc data, getter kích hoạt → Dep thu thập Watcher này
- Data thay đổi → setter kích hoạt → Dep notify các Watcher → View cập nhật
Chiều View → Model
- Compiler xử lý template, tạo giao diện và gắn sự kiện
- Người dùng tương tác → event callback thay đổi data
- Observer bắt thay đổi → Dep notify → View cập nhật theo data mới
Thực hành: Xây dựng MVVM từ đầu
Lớp MVVM chính
class MiniVue {
constructor(config) {
this.$config = config;
this.$root = config.el;
this.$state = config.data;
// Tạo proxy để truy cập trực tiếp property
this._buildProxy(this, "$state", this.$state);
// Khởi tạo reactive system
this._makeReactive(this.$state);
// Biên dịch template
new TemplateCompiler(this.$root, this);
}
_buildProxy(instance, path, source) {
const properties = Object.keys(source);
properties.forEach(prop => {
Object.defineProperty(instance, prop, {
get() {
return instance[path][prop];
},
set(updated) {
instance[path][prop] = updated;
}
});
});
}
_makeReactive(data) {
new ReactiveHandler(data);
}
}
class ReactiveHandler {
constructor(data) {
this._transform(data);
}
_transform(dataset) {
Object.keys(dataset).forEach(field => {
this._defineReactive(dataset, field, dataset[field]);
});
}
_defineReactive(dataset, field, initialValue) {
const dependency = new DependencyTracker();
if (initialValue !== null && typeof initialValue === "object") {
this._transform(initialValue);
}
Object.defineProperty(dataset, field, {
get() {
if (DependencyTracker.activeWatcher) {
dependency.registerWatcher();
}
return initialValue;
},
set(updatedValue) {
initialValue = updatedValue;
dependency.broadcastUpdate();
}
});
}
}
class DependencyTracker {
constructor() {
this.observers = [];
}
registerWatcher() {
if (DependencyTracker.activeWatcher) {
DependencyTracker.activeWatcher.attachTo(this);
}
}
addObserver(observer) {
this.observers.push(observer);
}
broadcastUpdate() {
this.observers.forEach(observer => observer.refresh());
}
}
DependencyTracker.activeWatcher = null;
class DataWatcher {
constructor(instance, expression, handler) {
this.instance = instance;
this.expression = expression;
this.handler = handler;
this.linkedDeps = [];
this.linkedDepIds = new Set();
this.pendingDeps = [];
this.pendingDepIds = new Set();
this.currentValue = this._evaluate();
this.handler(this.currentValue);
}
_evaluate() {
DependencyTracker.activeWatcher = this;
this.pendingDeps = [];
this.pendingDepIds = new Set();
let result = this.instance;
const pathSegments = this.expression.includes(".")
? this.expression.split(".")
: [this.expression];
pathSegments.forEach(segment => {
result = result[segment];
});
DependencyTracker.activeWatcher = null;
// Dọn dẹp dependency cũ không còn sử dụng
this.linkedDeps.forEach(dep => {
if (!this.pendingDepIds.has(dep.id)) {
dep.removeObserver(this);
}
});
this.linkedDeps = this.pendingDeps;
this.linkedDepIds = this.pendingDepIds;
return result;
}
attachTo(dep) {
if (!this.pendingDepIds.has(dep.id)) {
this.pendingDeps.push(dep);
this.pendingDepIds.add(dep.id);
if (!this.linkedDepIds.has(dep.id)) {
dep.addObserver(this);
}
}
}
refresh() {
this.currentValue = this._evaluate();
this.handler(this.currentValue);
}
}
Lớp biên dịch template
class TemplateCompiler {
constructor(selector, instance) {
this.domRoot = document.querySelector(selector);
this.instance = instance;
if (this.domRoot) {
// Tách DOM thành fragment để xử lý
this.domFragment = this._extractFragment(this.domRoot);
// Xử lý các directive và interpolation
this._processNodes(this.domFragment);
// Gắn lại vào DOM
this.domRoot.appendChild(this.domFragment);
}
}
_extractFragment(element) {
const fragment = document.createDocumentFragment();
let node;
while (element.firstChild && (node = element.firstChild)) {
fragment.appendChild(node);
}
return fragment;
}
_processNodes(fragment) {
const nodes = Array.from(fragment.childNodes);
nodes.forEach(node => {
const content = node.textContent;
const pattern = /\{\{(.*?)\}\}/g;
const nodeType = node.nodeType;
// Xử lý text node có interpolation
if (nodeType === 3 && pattern.test(content)) {
const expression = RegExp.$1.trim();
let dataValue = this.instance;
const keys = expression.split(".");
keys.forEach(key => {
dataValue = dataValue[key];
});
node.textContent = content.replace(pattern, dataValue);
// Theo dõi thay đổi để cập nhật giao diện
new DataWatcher(this.instance, expression, (newValue) => {
node.textContent = content.replace(pattern, newValue);
});
}
// Xử lý element node có directive
if (nodeType === 1) {
const attributes = Array.from(node.attributes);
attributes.forEach(attribute => {
const directiveName = attribute.nodeName;
const boundProperty = attribute.nodeValue;
if (directiveName.startsWith("v-")) {
const directiveType = directiveName.slice(2);
if (directiveType === "model") {
// Khởi tạo giá trị
node.value = this.instance[boundProperty];
// Lắng nghe input từ người dùng
node.addEventListener("input", event => {
this.instance[boundProperty] = event.target.value;
});
}
}
});
}
// Đệ quy xử lý các node con
if (node.childNodes && node.childNodes.length > 0) {
this._processNodes(node);
}
});
}
}
Cách sử dụng
<!DOCTYPE html>
<html lang="vi">
<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">
<h2>{{trackName}}</h2>
<p>Album: {{record.title}}</p>
<p>Nghệ sĩ: {{artist}}</p>
<input type="text" v-model="trackName" />
</div>
<script>
const app = new MiniVue({
el: "#app",
data: {
trackName: "Fairy Tale",
record: {
title: "Once",
year: 2004
},
artist: "Nightwish"
}
});
</script>
</body>
</html>
Đoạn code trên minh họa cách một framework MVVM đơn giản hoạt động: dữ liệu được đóng gói thành reactive, template được biên dịch để tạo binding hai chiều, và mọi thay đổi đều tự động lan truyền đến giao diện.