Build Your Own MVVM Framework from Scratch with JavaScript

This article walks through the complete implementation of a lightweight MVVM framework—covering Object.defineProperty data hijacking, proxying, template compilation, publish‑subscribe reactivity, two‑way binding, computed properties and lifecycle hooks—using plain JavaScript.

37 Mobile Game Tech Team
37 Mobile Game Tech Team
37 Mobile Game Tech Team
Build Your Own MVVM Framework from Scratch with JavaScript

MVVM Development

MVVM two‑way data binding originally used dirty checking in Angular 1.x, but modern frameworks such as React, Vue and Angular now rely on data hijacking combined with the publish‑subscribe pattern, implemented via Object.defineProperty. Compatibility issues with defineProperty explain why Vue only supports IE8+.

defineProperty

Although developers rarely call Object.defineProperty directly, it is essential when building frameworks or libraries.

let obj = {};
let hero = '鲁班二号';
obj.player = '小学生';
Object.defineProperty(obj, 'hero', {
    configurable: true,
    enumerable: true,
    get() { return hero; },
    set(val) { hero = val; }
});
console.log(obj); // {player: '小学生', hero: '鲁班二号'}
delete obj.hero; // requires configurable:true
obj.hero = '后羿'; // requires writable:true
for (let key in obj) { console.log(key); }
console.log(obj.hero); // '后羿'

The above demonstrates basic usage of defineProperty.

Building MVVM

HTML skeleton:

<body>
  <div id="app">
    <h1>{{hero}}</h1>
    <p>身高{{info.height}}、智商{{info.IQ}}的峡谷英雄</p>
    <p>操作他的玩家是{{player}}。</p>
  </div>
  <script src="mvvm.js"></script>
  <script>
    let mvvm = new Mvvm({
      el: '#app',
      data: {
        hero: '鲁班二号',
        info: { height: '150cm', IQ: 250 },
        player: '小学生'
      }
    });
  </script>
</body>

Constructor:

function Mvvm(options = {}) {
  this.$options = options;
  this._data = this.$options.data;
  observe(this._data); // data hijacking
}

Data Hijacking

Hijacking intercepts property access by defining getters and setters with Object.defineProperty. Recursion ensures deep objects are also observed.

function Observe(data) {
  for (let key in data) {
    let val = data[key];
    observe(val); // recursive deep hijacking
    Object.defineProperty(data, key, {
      configurable: true,
      get() { return val; },
      set(newVal) {
        if (val === newVal) return;
        val = newVal;
        observe(newVal);
      }
    });
  }
}
function observe(data) {
  if (!data || typeof data !== 'object') return;
  return new Observe(data);
}

Data Proxy

Proxy copies properties from this._data onto the MVVM instance, allowing mvvm.a.b instead of mvvm._data.a.b.

for (let key in data) {
  Object.defineProperty(this, key, {
    configurable: true,
    get() { return this._data[key]; },
    set(newVal) { this._data[key] = newVal; }
  });
}

Data Compilation

The compiler replaces {{}} expressions with actual data values and registers watchers.

function Compile(el, vm) {
  vm.$el = document.querySelector(el);
  let fragment = document.createDocumentFragment();
  while (child = vm.$el.firstChild) { fragment.appendChild(child); }
  function replace(frag) {
    Array.from(frag.childNodes).forEach(node => {
      let txt = node.textContent;
      let reg = /\{\{(.*?)\}\}/g;
      if (node.nodeType === 3 && reg.test(txt)) {
        function replaceTxt() {
          node.textContent = txt.replace(reg, (m, p) => {
            new Watcher(vm, p, replaceTxt);
            return p.split('.').reduce((v, k) => v[k], vm);
          });
        }
        replaceTxt();
      }
      if (node.childNodes && node.childNodes.length) replace(node);
    });
  }
  replace(fragment);
  vm.$el.appendChild(fragment);
}

Publish‑Subscribe

Dep maintains a list of subscriber functions; Watcher wraps a callback with an update method.

function Dep() { this.subs = []; }
Dep.prototype.addSub = function(sub) { this.subs.push(sub); };
Dep.prototype.notify = function() { this.subs.forEach(sub => sub.update()); };
function Watcher(fn) { this.fn = fn; }
Watcher.prototype.update = function() { this.fn(); };

Data‑View Synchronization

During compilation, each expression creates a Watcher that registers with the corresponding Dep. When a setter runs, dep.notify() triggers all watchers to update the DOM.

Object.defineProperty(data, key, {
  get() { Dep.target && dep.addSub(Dep.target); return val; },
  set(newVal) { if (val === newVal) return; val = newVal; observe(newVal); dep.notify(); }
});

Two‑Way Data Binding

Elements with v-model are bound to data; input events update the model, which in turn notifies watchers to refresh the view.

if (node.nodeType === 1) {
  Array.from(node.attributes).forEach(attr => {
    if (attr.name.includes('v-')) {
      node.value = vm[attr.value];
      new Watcher(vm, attr.value, newVal => { node.value = newVal; });
      node.addEventListener('input', e => { vm[attr.value] = e.target.value; });
    }
  });
}

Computed Properties & Mounted Hook

Computed properties are defined on the MVVM instance using getters; the mounted hook runs after initialization.

function initComputed() {
  let vm = this;
  let computed = this.$options.computed || {};
  Object.keys(computed).forEach(key => {
    Object.defineProperty(vm, key, {
      get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
      set() {}
    });
  });
}
function Mvvm(options = {}) {
  this.$options = options;
  this._data = options.data;
  observe(this._data);
  initComputed.call(this);
  new Compile(options.el, this);
  options.mounted && options.mounted.call(this);
}

Summary

Data hijacking via Object.defineProperty getters/setters.

Proxying data properties onto the MVVM instance.

Template compilation using {{}} syntax.

Publish‑subscribe mechanism to keep data and view in sync.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

frontendJavaScriptVuereactiveMVVMObject.defineProperty
37 Mobile Game Tech Team
Written by

37 Mobile Game Tech Team

37 Mobile Game Tech Team

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.