news 2026/2/19 5:58:35

手写一个简易的 MVVM 框架:数据劫持、模板编译与发布订阅的整合

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写一个简易的 MVVM 框架:数据劫持、模板编译与发布订阅的整合

手写一个简易 MVVM 框架:数据劫持、模板编译与发布订阅的整合

各位开发者朋友,大家好!今天我们来一起手写一个简易但完整的 MVVM 框架。这个框架虽然不复杂,但它融合了前端开发中最核心的三大技术点:

  1. 数据劫持(响应式原理)
  2. 模板编译(视图更新机制)
  3. 发布-订阅模式(状态同步机制)

我们将从零开始构建它,让你真正理解 Vue.js 这类框架底层是如何工作的。文章会以讲座形式展开,逻辑清晰、代码详实、语言自然,适合有一定 JavaScript 基础的同学阅读。


一、什么是 MVVM?

MVVM 是 Model-View-ViewModel 的缩写,是一种用于构建用户界面的设计模式:

层级职责
Model数据层,通常是 JS 对象或 API 返回的数据
ViewUI 层,HTML + CSS 构成的页面结构
ViewModel连接 Model 和 View 的桥梁,负责数据绑定和事件处理

在我们的框架中,ViewModel 就是我们要实现的核心对象 —— 它监听数据变化,并自动更新 DOM。


二、整体架构设计

我们先定义一个简单的入口类MVVM,它包含以下关键功能:

class MVVM { constructor(options) { this.$options = options; this.$data = options.data; // 1. 数据劫持:让 data 变成响应式的 observe(this.$data); // 2. 编译模板:将 {{xxx}} 替换为实际值 new Compile(this.$options.el, this); } }

接下来我们分步实现这三个模块:observe(数据劫持)、Compile(模板编译)、Watcher(发布订阅)


三、第一步:数据劫持(observe)

目标:让this.$data中的所有属性变成“可观察”的,一旦修改就能触发更新。

核心思想:

使用Object.defineProperty劫持每个属性的 getter/setter,当访问或修改时通知订阅者。

function observe(data) { if (!data || typeof data !== 'object') return; Object.keys(data).forEach(key => { defineReactive(data, key, data[key]); }); } function defineReactive(obj, key, val) { const dep = new Dep(); // 每个属性对应一个 Dep 实例 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 如果有 watcher 正在读取该属性,则添加到依赖列表 if (Dep.target) { dep.addSub(Dep.target); } return val; }, set(newVal) { if (newVal === val) return; val = newVal; // 数据变更后通知所有订阅者(watcher) dep.notify(); } }); }

这里引入了一个新的概念:Dep(依赖收集器)

class Dep { constructor() { this.subs = []; // 存储所有订阅者(watcher) } addSub(sub) { this.subs.push(sub); } notify() { this.subs.forEach(sub => sub.update()); } }

注意:Dep.target是一个全局变量,用来临时保存当前正在执行的 Watcher(后续详解)。

这一步完成后,任何对$data的访问都会被拦截,且赋值时能触发更新!


四、第二步:模板编译(Compile)

目标:解析 HTML 中的插值表达式{{xxx}},并将其替换为真实数据。

比如:

<div id="app"> <p>{{name}}</p> <p>{{age}}</p> </div>

我们要把它变成:

<div id="app"> <p>张三</p> <p>25</p> </div>

编译过程分为两步:

Step 1:遍历 DOM 节点,找到所有{{xxx}}表达式
class Compile { constructor(el, vm) { this.el = document.querySelector(el); this.vm = vm; // 把真实 DOM 移动到 fragment 中提高性能 this.fragment = this.nodeToFragment(this.el); // 编译 fragment 中的内容 this.compileElement(this.fragment); // 最终把 fragment 插入原容器 this.el.appendChild(this.fragment); } nodeToFragment(el) { const fragment = document.createDocumentFragment(); let child; while ((child = el.firstChild)) { fragment.appendChild(child); } return fragment; } compileElement(node) { if (node.nodeType === 1) { // 元素节点,如 <p>、<div> this.compileAttrs(node); } else if (node.nodeType === 3) { // 文本节点,如 "Hello {{name}}" this.compileText(node); } // 递归子节点 Array.from(node.childNodes).forEach(child => { this.compileElement(child); }); } compileAttrs(node) { const attrs = node.attributes; Array.from(attrs).forEach(attr => { const attrName = attr.name; const exp = attr.value; if (attrName.startsWith('v-bind:')) { const key = attrName.slice(7); // v-bind:name -> name this.bindAttr(node, key, exp); } }); } compileText(node) { const text = node.textContent.trim(); const reg = /{{(.+?)}}/g; // 匹配 {{xxx}} if (reg.test(text)) { node.textContent = text.replace(reg, (_, key) => { // 创建 watcher 监听这个 key 的变化 new Watcher(this.vm, key, (newVal) => { node.textContent = text.replace(reg, (_, k) => newVal); }); return this.vm.$data[key]; }); } } bindAttr(node, key, exp) { new Watcher(this.vm, exp, (newVal) => { node.setAttribute(key, newVal); }); node.setAttribute(key, this.vm.$data[exp]); } }

关键点说明:

  • 使用document.createDocumentFragment()避免频繁 DOM 操作。
  • compileText处理文本节点中的{{xxx}},并创建 Watcher。
  • bindAttr支持v-bind:绑定属性(例如<img src="{{url}}" />)。

现在,只要你在data中改了某个字段,对应的 DOM 就会自动刷新!


五、第三步:发布订阅(Watcher)

这是整个框架最精妙的部分 —— Watcher 是连接数据和视图的纽带。

Watcher 类定义如下:

class Watcher { constructor(vm, exp, cb) { this.vm = vm; this.exp = exp; this.cb = cb; // 当前 watcher 被 push 到 Dep.target 上 Dep.target = this; // 触发一次 getter 获取初始值(同时触发 dep.addSub) this.value = this.get(); Dep.target = null; } get() { return this.vm.$data[this.exp]; // 触发 defineReactive 中的 getter } update() { const newVal = this.get(); if (newVal !== this.value) { this.cb(newVal); // 更新回调函数 this.value = newVal; } } }

工作流程总结:

  1. 创建 Watcher 时,设置Dep.target = this
  2. 执行this.get()→ 触发defineReactive.get()→ 添加当前 Watcher 到 Dep 的 subs 列表
  3. 后续数据变更 →dep.notify()→ 所有 Watcher 执行update()
  4. update()中调用用户传入的回调函数(如更新 DOM)

这就是经典的观察者模式(Observer Pattern)


六、完整示例演示

让我们用一个完整的例子验证整个框架是否正常工作:

<!DOCTYPE html> <html> <head> <title>MVVM Demo</title> </head> <body> <div id="app"> <h1>{{title}}</h1> <p>姓名:<span v-bind:text="name"></span></p> <p>年龄:<span>{{age}}</span></p> <button onclick="app.change()">改变数据</button> </div> <script> class MVVM { constructor(options) { this.$options = options; this.$data = options.data; observe(this.$data); new Compile(this.$options.el, this); } change() { this.$data.name = "李四"; this.$data.age += 1; } } const app = new MVVM({ el: '#app', data: { title: '我的应用', name: '张三', age: 25 } }); window.app = app; </script> </body> </html>

运行效果:

  • 页面显示:“我的应用”、“张三”、“25”
  • 点击按钮后:
    • 名字变为 “李四”
    • 年龄加 1,变为 26
    • 自动更新 DOM,无需手动操作!

整个过程完全由框架内部完成,你只需要关心业务数据!


七、对比传统做法 vs MVVM 框架

方式缺点MVVM 解决方案
手动操作 DOM(如document.getElementById(...).innerHTML = xxx易出错、难以维护数据驱动视图,减少手动 DOM 操作
事件监听 + DOM 更新分离逻辑混乱、耦合度高Watcher 统一管理数据变更与视图同步
不支持双向绑定需要额外逻辑处理输入框同步可扩展为双向绑定(只需加 input 监听)

八、优化建议 & 扩展方向

目前框架已经具备基础能力,但可以进一步增强:

功能实现思路
双向绑定(v-model)监听 input 输入事件,同步到 data;data 变化也同步回 input
计算属性(computed)将 computed 字段作为 Watcher,缓存结果避免重复计算
生命周期钩子如 mounted、updated,提供 hook 函数供用户自定义行为
指令系统(v-if / v-for)扩展 Compile 类,支持更多语法糖
异步更新队列避免多次 set 引起的频繁渲染,合并成一次批量更新

这些都可以基于现有结构轻松扩展!


九、总结

今天我们亲手打造了一个简易但完整的 MVVM 框架,其核心在于:

  1. 数据劫持(observe):通过Object.defineProperty实现响应式;
  2. 模板编译(Compile):识别并处理{{xxx}}v-bind:
  3. 发布订阅(Watcher + Dep):建立数据与视图之间的通信链路。

这套机制正是 Vue.js 的前身,也是现代前端框架(React Hooks、Svelte 等)背后的通用思想。

推荐学习路径:

  • 先理解本文内容,再看 Vue 源码(尤其是observercompilerwatcher
  • 尝试自己加上v-modelcomputedfilter等特性
  • 最终目标是掌握“如何从无到有搭建一个小型前端框架”

希望这篇文章能帮你打通前端框架的理解壁垒,不再只是“会用”,而是“懂原理”。

谢谢大家!欢迎留言交流

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/14 20:17:53

第1节:项目性能优化(上)

本章学习目标&#xff1a; 了解应用性能问题分析方法论&#xff1b;掌握压力测试基础概念&#xff1b;掌握压力测试&#xff1a;线程组配置&#xff0c;结果分析&#xff0c;插件使用&#xff1b;理解性能关键的指标&#xff1b; 性能问题分析方法论 首先我们需要知道性能优化…

作者头像 李华
网站建设 2026/2/17 6:48:52

学习日记day51

Day51_1216专注时间&#xff1a;2H59min每日任务&#xff1a;2h复习数据库&#xff08;完成情况及时长&#xff1a;&#xff09;&#xff1b;1h二刷2道力扣hot100(如果是hard&#xff0c;只做一道就好&#xff0c;完成情况及时长&#xff1a;今天都在做算法题&#xff0c;也懈怠…

作者头像 李华
网站建设 2026/2/3 9:00:40

FlutterOpenHarmony商城App订单列表组件开发

前言 订单列表是商城应用中用户查看和管理订单的核心页面&#xff0c;用户可以在这里查看所有订单的状态、进行订单操作如取消、确认收货、申请退款等。一个设计良好的订单列表组件需要清晰地展示订单信息&#xff0c;并提供便捷的操作入口。本文将详细介绍如何在Flutter和Open…

作者头像 李华
网站建设 2026/2/13 8:09:15

了解陇南支腿凿岩机出厂行情查询报价享折扣

在矿山、隧道及大型基建工程中&#xff0c;凿岩设备的选型常因需求错配与参数混乱而陷入低效甚至停工风险。面对风动凿岩机、手持式气动凿岩机、气腿式凿岩机等众多品类&#xff0c;用户往往难以精准匹配作业场景与设备性能——尤其在陇南这类地形复杂、岩石硬度多变的区域&…

作者头像 李华
网站建设 2026/2/5 11:13:00

金仓新势力:不止兼容,三重革新引领数据库未来

兼容 是对企业历史投资的尊重 是确保业务平稳过渡的基石 然而 这仅仅是故事的起点 在数字化转型的深水区&#xff0c;企业对数据库的需求早已超越“语法兼容”的基础诉求。无论是核心业务系统的稳定运行&#xff0c;还是敏感数据的安全防护&#xff0c;亦或是复杂场景下的性能优…

作者头像 李华
网站建设 2026/2/3 22:08:03

AI agent 最新 进展

AI Agent 最新进展&#xff08;2025 年 12 月&#xff09; 一、巨头竞相发布新一代 Agent 1. 谷歌&#xff1a;Gemini Deep Research Agent&#xff08;12 月 11 日&#xff09; 性能突破&#xff1a;在 "人类最后的考试"(HLE) 测试中达46.4%&#xff0c;超越 GPT-5 …

作者头像 李华