Phân tích cơ chế MVVM trong Vue 2 từ gốc

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:

  1. Model → View: Đổi dữ liệu thành giao diện qua data binding
  2. 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

  1. Observer chuyển data thành reactive qua Object.defineProperty
  2. Khi Watcher đọc data, getter kích hoạt → Dep thu thập Watcher này
  3. Data thay đổi → setter kích hoạt → Dep notify các Watcher → View cập nhật

Chiều View → Model

  1. Compiler xử lý template, tạo giao diện và gắn sự kiện
  2. Người dùng tương tác → event callback thay đổi data
  3. 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.

Thẻ: Vue.js Vue 2 Reactivity System Object.defineProperty Two-way Binding

Đăng vào ngày 26 tháng 10 lúc 06:35