Vue Reactivity System Implementation

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

Publish-Subscribe Diagram

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

Reactivity Flowchart

Tags: vue Reactivity javascript Observer Pattern

Posted on Sat, 03 Oct 2026 16:13:59 +0000 by salomo