简单实现MVVM框架,轻上手,简单易懂

引言

在前端开发中,MVVM(Model-View-ViewModel)框架如 Vue.js、React 等,极大地提升了布局与逻辑分离、数据驱动视图更新的效率。为了深入理解其背后的原理,我手动实现一个简化版 MVVM 框架。通过下面这个小项目,根据自己理解从零构建一个具备响应式数据绑定、模板编译与更新机制的 MVVM 框架。菜鸟入门,如有错误,大佬请指正(骂轻点)。


项目简介

  • 技术栈:ES6+ JavaScript

  • 功能

    1. 数据响应式:对数据对象进行劫持,实现数据变化触发视图更新
    2. 模板编译:解析带有指令和插值的模板,绑定到数据
    3. 发布-订阅:实现依赖收集和通知机制

MVVM 原理概览

  1. Model(数据层) :原始数据对象,通过代理或定义访问器,实现监听与更新。
  2. View(视图层) :用户界面模板,通常是含有特殊指令或插值的 HTML。
  3. ViewModel(视图-数据连接层) :核心桥梁,完成模板编译、依赖收集,并在 Model 变化时更新 View。

关键技术点:

  • 数据劫持(Object.defineProperty 或 Proxy)
  • 依赖收集(Dep/Watcher)
  • 模板编译(解析指令、插值表达式)
  • 更新机制(批量更新或同步更新)

核心模块设计

1. Observer(数据劫持)

使用 Object.defineProperty(兼容性更好)或 Proxy 对数据对象进行深度遍历,并在属性的 getset 方法中:

  • get 时收集依赖
  • set 时通知依赖更新
javascript 复制代码
class Observer {
  constructor(data) {
    this.walk(data);
  }

  walk(obj) {
    Object.keys(obj).forEach(key => this.defineReactive(obj, key, obj[key]));
  }

  defineReactive(obj, key, val) {
    // 递归处理嵌套对象
    if (typeof val === 'object' && val !== null) {
      new Observer(val);
    }
    const dep = new Dep();
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        Dep.target && dep.addSub(Dep.target);
        return val;
      },
      set(newVal) {
        if (newVal === val) return;
        val = newVal;
        // 新值为对象则继续监听
        if (typeof newVal === 'object') {
          new Observer(newVal);
        }
        dep.notify();
      }
    });
  }
}

2. Dep 与 Watcher(发布-订阅)

  • Dep :维护一个订阅者数组,提供 addSubnotify 方法。
  • Watcher:代表一个观察者。每当依赖属性变化,调用更新方法,触发视图更新。
javascript 复制代码
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(watcher) {
    this.subs.push(watcher);
  }
  notify() {
    this.subs.forEach(w => w.update());
  }
}

class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    Dep.target = this;
    // 触发一次 getter,完成依赖收集
    this.value = this.get();
    Dep.target = null;
  }

  get() {
    const keys = this.expr.split('.');
    let val = this.vm.$data;
    keys.forEach(k => val = val[k]);
    return val;
  }

  update() {
    const newVal = this.get();
    const oldVal = this.value;
    if (newVal !== oldVal) {
      this.value = newVal;
      this.cb(newVal);
    }
  }
}

3. Compiler(模板编译)

解析模板中所有文本节点与指令(如 v-modelv-textv-on 等),并为每个绑定创建对应的 Watcher。

javascript 复制代码
class Compiler {
  constructor(el, vm) {
    this.el = document.querySelector(el);
    this.vm = vm;
    this.fragment = this.node2Fragment(this.el);
    this.compile(this.fragment);
    this.el.appendChild(this.fragment);
  }

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

  compile(node) {
    node.childNodes.forEach(child => {
      if (child.nodeType === 1) {
        // 元素节点
        this.compileElement(child);
      } else if (child.nodeType === 3) {
        // 文本节点
        this.compileText(child);
      }
      if (child.childNodes && child.childNodes.length) {
        this.compile(child);
      }
    });
  }

  compileElement(node) {
    Array.from(node.attributes).forEach(attr => {
      const { name, value } = attr;
      if (name.startsWith('v-')) {
        const [, directive] = name.split('-');
        CompilerUtil[directive](node, this.vm, value);
      }
    });
  }

  compileText(node) {
    const reg = /{{(.+?)}}/g;
    const text = node.textContent;
    if (reg.test(text)) {
      CompilerUtil['text'](node, this.vm, text);
    }
  }
}

const CompilerUtil = {
  text(node, vm, expr) {
    const value = expr.replace(/{{(.+?)}}/g, (_, g) => {
      new Watcher(vm, g.trim(), newVal => {
        this.updater.textUpdater(node, this.getVal(vm, g.trim()));
      });
      return this.getVal(vm, g.trim());
    });
    this.updater.textUpdater(node, value);
  },
  model(node, vm, expr) {
    const val = this.getVal(vm, expr);
    new Watcher(vm, expr, newVal => {
      this.updater.modelUpdater(node, newVal);
    });
    node.addEventListener('input', e => {
      this.setVal(vm, expr, e.target.value);
    });
    this.updater.modelUpdater(node, val);
  },
  getVal(vm, expr) {
    return expr.split('.').reduce((data, key) => data[key], vm.$data);
  },
  setVal(vm, expr, value) {
    expr.split('.').reduce((data, key, i, arr) => {
      if (i === arr.length - 1) data[key] = value;
      return data[key];
    }, vm.$data);
  },
  updater: {
    textUpdater(node, value) {
      node.textContent = value;
    },
    modelUpdater(node, value) {
      node.value = value;
    }
  }
};

4. Vue 类入口

javascript 复制代码
class MyVue {
  constructor(options) {
    this.$el = options.el;
    this.$data = options.data;
    // 1. 数据劫持
    new Observer(this.$data);
    // 2. 模板编译与挂载
    new Compiler(this.$el, this);
  }
}

项目结构示例

project 复制代码
mvvm-demo/
├── index.html   // 包含模板标记及引用脚本
├── mvvm.js      // 包含 Observer, Dep, Watcher, Compiler, MyVue
└── README.md

使用示例

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>MVVM Demo</title>
</head>
<body>
  <div id="app">
    <p>{{ message }}</p>
    <input type="text" v-model="message">
  </div>
  <script src="mvvm.js"></script>
  <script>
    const vm = new MyVue({
      el: '#app',
      data: {
        message: 'Hello MVVM'
      }
    });
  </script>
</body>
</html>

总结

通过以上步骤,我们实现了一个最简化的 MVVM 框架:它支持数据劫持、依赖收集、模板编译和双向绑定。这个小项目有助于理解 Vue.js 等框架内部的核心机制。未来可扩展指令系统、性能优化与批量更新策略,实现更完整的功能。

相关推荐
小小小小宇16 分钟前
PC和WebView白屏检测
前端
天天扭码28 分钟前
ES6 Symbol 超详细教程:为什么它是避免对象属性冲突的终极方案?
前端·javascript·面试
小矮马31 分钟前
React-组件和props
前端·javascript·react.js
懒羊羊我小弟35 分钟前
React Router v7 从入门到精通指南
前端·react.js·前端框架
DC...1 小时前
vue滑块组件设计与实现
前端·javascript·vue.js
Mars狐狸1 小时前
AI项目改用服务端组件实现对话?包体积减小50%!
前端·react.js
H5开发新纪元1 小时前
Vite 项目打包分析完整指南:从配置到优化
前端·vue.js
嘻嘻嘻嘻嘻嘻ys1 小时前
《Vue 3.3响应式革新与TypeScript高效开发实战指南》
前端·后端
恋猫de小郭2 小时前
腾讯 Kuikly 正式开源,了解一下这个基于 Kotlin 的全平台框架
android·前端·ios
2301_799404912 小时前
如何修改npm的全局安装路径?
前端·npm·node.js