Frontend Development 14 min read

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.

<code>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); // '后羿'
</code>

The above demonstrates basic usage of

defineProperty

.

Building MVVM

HTML skeleton:

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

Constructor:

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

Data Hijacking

Hijacking intercepts property access by defining getters and setters with

Object.defineProperty

. Recursion ensures deep objects are also observed.

<code>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);
}
</code>

Data Proxy

Proxy copies properties from

this._data

onto the MVVM instance, allowing

mvvm.a.b

instead of

mvvm._data.a.b

.

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

Data Compilation

The compiler replaces

{{}}

expressions with actual data values and registers watchers.

<code>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);
}
</code>

Publish‑Subscribe

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

update

method.

<code>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(); };
</code>

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.

<code>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(); }
});
</code>

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.

<code>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; });
    }
  });
}
</code>

Computed Properties & Mounted Hook

Computed properties are defined on the MVVM instance using getters; the

mounted

hook runs after initialization.

<code>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);
}
</code>

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.

FrontendJavaScriptvueReactiveMVVMData BindingObject.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

login 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.