Building a Mini Vue: Implementing Observer, Watcher, and Compiler

Vue's source code implements dependency collection using the Observer pattern. It involves three main classes:

  • Dep: Acts as the observable target. Every data property has a Dep instance containing a subs queue (short for subscribers) that holds all Watcher instances depending on the data. When data changes, dep.notify() is called to inform all watchers.
  • Watcher: Acts as the observer. It wraps observer functions (e.g., render()) into Watcher instances.
  • Observer: A helper class that makes arrays and objects observable by converting them.

The Observer pattern defines a one-to-many dependency between objects sothat when one object changes state, all its dependents are notified and updated automatically.

Core Concepts

Observer Pattern Overview

  • Intent: Define a one-to-many dependency between objects; when one object changes state, all dependents are notified and updated.
  • Problem: An object's state change needs to be communicated to other objects without tight coupling.
  • Solution: Use an abstract subject that maintains a list of observers. Concrete subjects notify observers when state changes.
  • Key Code: An abstract class holds an ArrayList of observers.
  • Usage Examples: Auction (auctioneer notifies bidders); event handling systems.

Implementation

Below is a simplified implementation demonstrating the pattern:

<!DOCTYPE html>
<html lang="en">
<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">
    <div @click="getValue">
      <p k-text="inputData"></p>
    </div>
    <p @click="getValue">Hello {{ form.test }} you {{ form.test }} {{ obj.a.c }}</p>
    <p>{{ inputData }}</p>
    <input :value="inputData" @input="setInput" />
  </div>

  <script src="./kvue.js"></script>
  <script>
    new Kvue({
      el: 'app',
      data: {
        form: { test: '1124dsa' },
        inputData: '',
        test: 111,
        obj: { a: { c: 3 } }
      },
      watch: {
        inputData(val) { console.log(val) }
      },
      created() {
        console.log(this.test)
        console.log(this.obj)
      },
      methods: {
        getValue() { console.log(this) },
        setInput(event) { this.inputData = event.target.value }
      }
    })
  </script>
</body>
</html>
// Helper functions
function getData(data, vm) {
  return data.call(vm, vm);
}

function parsePath(path) {
  const segments = path.split('.');
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return undefined;
      obj = obj[segments[i]];
    }
    return obj;
  };
}

function initMethods(vm, methods) {
  for (let key in methods) {
    vm[key] = typeof methods[key] !== 'function' ? function(){} : methods[key].bind(vm);
  }
}

function initWatch(vm, watch) {
  for (let key in watch) {
    new Watcher(vm, key, watch[key]);
  }
}

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/;

class Kvue {
  constructor(options) {
    let data = options.data;
    this.$data = typeof data === 'function' ? getData(data, this) : data || {};
    const keys = Object.keys(this.$data);
    let i = keys.length;
    while (i--) {
      this.proxyData(keys[i]);
    }
    this.observe(this.$data);
    initWatch(this, options.watch);
    initMethods(this, options.methods);
    new Compile(this, options.el);
    if (options.created) {
      options.created.call(this);
    }
  }

  observe(obj) {
    if (!obj || Object.prototype.toString.call(obj) !== '[object Object]') return;
    Object.keys(obj).forEach(key => {
      this.defineReactive(obj, key, obj[key]);
    });
  }

  defineReactive(obj, key, val) {
    this.observe(val); // Recursively make nested objects reactive
    const dep = new Dep();
    Object.defineProperty(obj, key, {
      get() {
        if (Dep.target) dep.addSub(Dep.target);
        return val;
      },
      set(newVal) {
        if (newVal === val) return;
        val = newVal;
        dep.notify();
      }
    });
  }

  proxyData(key) {
    Object.defineProperty(this, key, {
      get() { return this.$data[key]; },
      set(newVal) { this.$data[key] = newVal; }
    });
  }
}

class Dep {
  constructor() {
    this.subs = [];
  }

  addSub(sub) {
    this.subs.push(sub);
  }

  notify() {
    this.subs.forEach(watcher => watcher.update());
  }
}

Dep.target = null;

class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    Dep.target = this;
    parsePath(this.key)(this.vm); // Access property to trigger getter and collect dependency
    Dep.target = null;
    this.update();
  }

  update() {
    this.cb.call(this.vm, parsePath(this.key)(this.vm));
  }
}

class Compile {
  constructor(vm, el) {
    this.$vm = vm;
    this.$el = document.getElementById(el);
    if (this.$el) {
      this.$fragment = this.nodeToFragment(this.$el);
      this.compile(this.$fragment);
      this.$el.appendChild(this.$fragment);
    }
  }

  nodeToFragment(el) {
    const frag = document.createDocumentFragment();
    let child;
    while (child = el.firstChild) {
      frag.appendChild(child);
    }
    return frag;
  }

  compile(frag) {
    const nodes = Array.from(frag.childNodes);
    nodes.forEach(node => {
      if (this.isElementNode(node)) {
        const attrs = Array.from(node.attributes);
        attrs.forEach(attr => {
          const name = attr.name;
          const value = attr.value;
          if (this.isDirective(name)) {
            const directive = name.substring(2);
            if (this[directive]) this[directive](node, this.$vm, value);
          }
          if (this.isEvent(name)) {
            const event = name.substring(1);
            this.eventHandler(node, this.$vm, event, value);
          }
        });
      }
      if (this.isTextNode(node)) {
        this.textNodeHandler(node, this.$vm);
      }
      if (node.childNodes && node.childNodes.length) {
        this.compile(node);
      }
    });
  }

  update(node, vm, exp, type) {
    const updateFn = this[`update${type}`];
    new Watcher(vm, exp, function(value) {
      if (updateFn) updateFn(node, value);
    });
  }

  isElementNode(node) { return node.nodeType === 1; }
  isTextNode(node) { return node.nodeType === 3; }
  isDirective(attrName) { return attrName.startsWith('k-'); }
  isEvent(attrName) { return attrName.startsWith('@'); }

  text(node, vm, exp) {
    this.update(node, vm, exp, 'Text');
  }

  textNodeHandler(node, vm) {
    const match = defaultTagRE.exec(node.textContent);
    if (match) {
      const exp = match[1].trim();
      this.update(node, vm, exp, 'TextNode');
      this.textNodeHandler(node, vm); // Handle multiple interpolations
    }
  }

  updateText(node, value) {
    node.textContent = value;
  }

  updateTextNode(node, value) {
    const content = node.textContent;
    if (content) {
      node.textContent = content.replace(defaultTagRE, value);
    }
  }

  eventHandler(node, vm, event, exp) {
    const fn = vm[exp];
    node.addEventListener(event, fn);
  }
}

The implementation demonstrates:

  1. Data reactivity: defineReactive makes each property observable via Object.defineProperty.
  2. Dependency tracking: Dep collects active Watcher instances during property access.
  3. Compilation: Compile parses templates, binds directives and events, and creates watchers for reactive updates.
  4. Watcher: Re-evaluates expressions and triggers updates when dependencies change.

This mini Vue captures the essence of Vue's reactive system and template compilation.

Tags: vue Observer Pattern Reactivity Dependency Injection Mini Framework

Posted on Mon, 24 Aug 2026 16:25:35 +0000 by rcoursey