实现一个简单的前端MVVM框架类似VUE

news2024/9/25 11:15:29

在本篇博客中,我们将使用原生JavaScript实现一个简单的前端MVVM框架,类似于VUE。MVVM是Model-View-ViewModel的缩写,是一种用于构建现代化、可维护的前端应用程序的架构模式。MVVM框架通过数据绑定和组件化的方式实现了视图与数据的双向绑定,使得数据的变化可以自动反映在视图上,同时视图的变化也会自动更新数据,从而实现了数据和视图的同步更新。

本篇博客将分为多个部分来介绍实现MVVM框架的过程。首先,我们会介绍MVVM框架的基本原理和核心概念。然后,我们会逐步实现MVVM框架的各个功能模块,包括数据劫持、编译模板、观察者和依赖收集等。最后,我们会通过一个简单的示例来演示MVVM框架的使用。

pic

1. MVVM框架基本原理和核心概念

MVVM框架是一种基于数据驱动的前端框架,它的核心概念包括:

  • Model(模型):代表应用程序的数据和业务逻辑。在MVVM框架中,Model通常是一个JavaScript对象,用于存储应用程序的数据。

  • View(视图):代表用户界面。在MVVM框架中,View通常是HTML模板,用于展示数据。

  • ViewModel(视图模型):是View和Model之间的连接层。ViewModel负责将Model的数据转换成View可以显示的数据,并监听View中的事件,当View发生变化时,更新Model中的数据。

MVVM框架通过数据绑定和组件化的方式实现了View和Model之间的双向绑定。当Model中的数据发生变化时,View会自动更新;当View中的数据发生变化时,Model会自动更新。这种双向绑定机制使得数据和视图始终保持同步,大大简化了前端开发的复杂性。

2. 实现Observer:数据劫持

 数据劫持是MVVM框架的核心功能之一,它通过拦截对象的属性访问来实现对数据的监控。在我们的MVVM框架中,我们将使用Observer类来实现数据劫持功能。

// observer.js

// 定义Dep类,用于收集依赖和通知更新
class Dep {
  constructor() {
    this.subs = {};
  }

  addSub(target) {
    this.subs[target.uid] = target;
  }

  notify() {
    for (let uid in this.subs) {
      this.subs[uid].update();
    }
  }
}

// 定义Observer类,用于实现数据劫持
export default class Observer {
  constructor(data) {
    this.data = data;
    this.walk(this.data);
  }

  walk(data) {
    if (!data || typeof data !== "object") {
      return;
    }
    Object.keys(data).forEach((key) => {
      this.defineReactive(data, key, data[key]);
    });
  }

  defineReactive(data, key, value) {
    var dep = new Dep();
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: false,
      get: () => {
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      set: (newValue) => {
        value = newValue;
        dep.notify();
      },
    });
    this.walk(value);
  }
}

在上述代码中,我们定义了Dep类用于收集依赖和通知更新,以及Observer类用于实现数据劫持功能。Dep类中的subs属性用于存储所有的Watcher实例,它会在get方法中被使用来收集依赖,在notify方法中被使用来通知更新。Observer类的构造函数接受一个data参数,用于指定要劫持的数据对象。walk方法用于遍历对象数据并调用defineReactive方法对每个属性进行劫持。

defineReactive方法中,我们使用Object.defineProperty来定义对象的属性,拦截对属性的访问和修改。在get方法中,我们将Dep.target(当前的Watcher实例)添加到对应的依赖中,以便在属性发生变化时能够通知更新;在set方法中,当属性发生变化时,我们将通知所有的依赖进行更新。

3. 实现Compiler:模板编译

模板编译是MVVM框架的另一个重要功能,它通过解析模板中的特殊符号(例如{{}}、v-model、v-text等)来实现对视图的更新。在我们的MVVM框架中,我们将使用Compiler类来实现模板编译功能。

// compiler.js

import Watcher from "./watcher";

export default class Compiler {
  constructor(context) {
    this.$el = context.$el;
    this.context = context;
    if (this.$el) {
      this.$fragment = this.nodeToFragment(this.$el);
      this.compiler(this.$fragment);
      this.$el.appendChild(this.$fragment);
    }
  }

  nodeToFragment(node) {
    let fragment = document.createDocumentFragment();
    if (node.childNodes && node.childNodes.length) {
      node.childNodes.forEach((child) => {
        if (!this.ignorable(child)) {
          fragment.appendChild(child);
        }
      });
    }
    return fragment;
  }

  ignorable(node) {
    var reg = /^[\t\n\r]+/;
    return (
      node.nodeType === 8 || (node.nodeType === 3 && reg.test(node.textContent))
    );
  }

  compiler(fragment) {
    if (fragment.childNodes && fragment.childNodes.length) {
      fragment.childNodes.forEach((child) => {
        if (child.nodeType === 1) {
          this.compilerElementNode(child);
        } else if (child.nodeType === 3) {
          this.compilerTextNode(child);
        }
      });
    }
  }

  compilerElementNode(node) {
    let attrs = [...node.attributes];
    attrs.forEach((attr) => {
      let { name: attrName, value: attrValue } = attr;
      if (attrName.indexOf("v-") === 0) {
        let dirName = attrName.slice(2);
        switch (dirName) {
          case "text":
            new Watcher(attrValue, this.context, (newValue) => {
              node.textContent = newValue;
            });
            break;
          case "model":
            new Watcher(attrValue, this.context, (newValue) => {
              node.value = newValue;
            });
            node.addEventListener("input", (e) => {
              this.context[attrValue] = e.target.value;
            });
            break;
        }
      }
    });
    this.compiler(node);
  }

  compilerTextNode(node) {
    let text = node.textContent.trim();
    if (text) {
      let exp = this.parseTextExp(text);
      new Watcher(exp, this.context, (newValue) => {
        node.textContent = newValue;
      });
    }
  }

  parseTextExp(text) {
    let regText = /\{\{(.+?)\}\}/g;
    var pices = text.split(regText);
    var matches = text.match(regText);
    let tokens = [];
    pices.forEach((item) => {
      if (matches && matches.indexOf("{{" + item + "}}") > -1) {
        tokens.push("(" + item + ")");
      } else {
        tokens.push("`" + item + "`");
      }
    });
    return tokens.join("+");
  }
}

在上述代码中,我们定义了Compiler类用于实现模板编译功能。Compiler类的构造函数接受一个context参数,用于指定MVVM框架的实例对象。在构造函数中,我们将MVVM框架的根元素$el转换为文档片段,并调用compiler方法对模板进行编译。编译过程中,我们会对每个元素节点和文本节点进行解析,识别特殊符号(例如v-model和v-text),并创建对应的Watcher实例来实现数据的响应式更新。

4. 实现Watcher和Dep:观察者和依赖收集

观察者和依赖收集是MVVM框架的关键部分,它们用于观察数据的变化并执行相应的更新。在我们的MVVM框架中,我们将使用WatcherDep类来实现观察者和依赖收集功能。

// dep.js

export default class Dep {
  constructor() {
    this.subs = {};
  }

  addSub(target) {
    this.subs[target.uid] = target;
  }

  notify() {
    for (let uid in this.subs) {
      this.subs[uid].update();
    }
  }
}

// watcher.js

import Dep from "./dep";

var $uid = 0;
export default class Watcher {
  constructor(exp, scope, cb) {
    this.exp = exp;
    this.scope = scope;
    this.cb = cb;
    this.uid = $uid++;
    this.update();
  }

  get() {
    Dep.target = this;
    let newValue = Watcher.computeExpression(this.exp, this.scope);
    Dep.target = null;
    return newValue;
  }

  update() {
    let newValue = this.get();
    this.cb && this.cb(newValue);
  }

  static computeExpression(exp, scope) {
    let fn = new Function("scope", "with(scope){return " + exp + "}");
    return fn(scope);
  }
}

在上述代码中,我们定义了Dep类用于收集依赖和通知更新,以及Watcher类用于观察数据的变化并执行相应的更新。Dep类的subs属性用于存储所有的Watcher实例,它会在get方法中被使用来收集依赖,在notify方法中被使用来通知更新。Watcher类的uid属性用于分配唯一的标识符,确保每个Watcher实例的唯一性。Watcher类的exp属性用于保存要观察的数据表达式,scope属性用于保存观察的作用域,cb属性用于保存更新数据的回调函数。

5. 实现Vue:MVVM框架类似VUE

最后,我们将使用Vue类来整合以上实现的功能,完成一个简单的MVVM框架类似VUE的效果。

// vue.js

import Observer from "./observer";
import Compiler from "./compiler";

class Vue {
  constructor(options) {
    this.$el = document.querySelector(options.el);
    this.$data = options.data || {};

    this._proxyData(this.$data);
    this._proxyMethods(options.methods);

    new Observer(this.$data);
    new Compiler(this);
  }

  _proxyData(data) {
    Object.keys(data).forEach((key) => {
      Object.defineProperty(this, key, {
        set(newValue) {
          data[key] = newValue;
        },
        get() {
          return data[key];
        },
      });
    });
  }

  _proxyMethods(methods) {
    if (methods && typeof methods === "object") {
      Object.keys(methods).forEach((key) => {
        this[key] = methods[key];
      });
    }
  }
}

window.Vue = Vue;

在上述代码中,我们定义了Vue类用于实现MVVM框架类似VUE的效果。Vue类的构造函数接受一个options参数,其中包含了MVVM框架的配置信息。在构造函数中,我们将MVVM框架的根元素$el转换为文档片段,并调用Compiler类对模板进行编译,同时使用Observer类对数据进行劫持,从而实现了MVVM框架的基本功能。

6. 示例

现在,我们可以使用我们实现的MVVM框架来创建一个简单的示例。首先,我们需要在HTML中引入我们的MVVM框架和示例数据,并指定根元素。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../dist/vue.js"></script>
    <!-- <script src="../src/index.js"></script> -->
  </head>
  <body>
    <div id="app">
      <p>111-{{msg + ' Vue'}}-222</p>
      <p v-text="msg"></p>
      <input type="text" v-model="msg" />
      <button @click="handleClick">click</button>
    </div>
    <script type="text/javascript">
      var vm = new Vue({
        el: "#app",
        data: {
          msg: "Hello",
          info: {
            a: "111",
          },
        },
        methods: {
          handleClick: function () {
            console.log("handleClick", this.msg);
          },
        },
      });
    </script>
  </body>
</html>

在上述示例中,我们使用了{{}}语法来显示数据,并使用v-model@click指令来实现数据的双向绑定和事件监听。当用户在输入框中输入内容时,数据会自动更新;当点击按钮时,数据会发生变化。

以上就是我们实现的简单前端MVVM框架类似VUE的过程。通过数据劫持、模板编译、观察者和依赖收集等功能的实现,我们实现了一个具备基本MVVM功能的前端框架。当然,实际的MVVM框架比这个示例要复杂得多,但是这个简单的实现已经展示了MVVM框架的核心原理和实现思路。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/805902.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Docker】使用docker-maven-plugin插件构建发布推镜像到私有仓库

文章目录 1. 用docker-maven-plugin插件推送项目到私服docker1.1. 构建镜像 v1.01.2. 构建镜像 v2.01.3. 推送到镜像仓库 2. 拉取私服docker镜像运行3. 参考资料 本文描述了在Spring Boot项目中通过docker-maven-plugin插件把项目推送到私有docker仓库中&#xff0c;随后拉取仓…

读发布!设计与部署稳定的分布式系统(第2版)笔记25_互联层之路由和服务

1. 控制请求数量 1.1. 这个世界可以随时摧毁我们的系统 1.1.1. 要么拒绝工作 1.1.2. 要么扩展容量 1.1.3. 没有人会在与世隔绝的环境中使用服务&#xff0c;现在的服务大多必须处理互联网规模的负载 1.2. 系统的每次失效&#xff0c;都源自某个等待队列 1.3. 每个请求都会…

【雕爷学编程】Arduino动手做(97)---10段LED光柱模块2

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

C#,数值计算——逻辑斯谛分布(logistic distribution)的计算方法与源程序

逻辑斯谛分布即增长分布&#xff0c;增长分布的分布函数是“增长函数”&#xff0c;亦称“逻辑斯谛函数”(logistic function)&#xff0c;故增长分布亦称做“逻辑斯谛分布”。逻辑斯谛分布(logistic distribution)是一种连续型的概率分布&#xff0c;记为L(μ,γ)&#xff0c;…

#systemverilog# 说说Systemverilog中《automatic》那些事儿

前面我们学习了有关systemverilog语言中有关《static》的一些知识,同static 关系比较好的哥们,那就是 《automatic》。今天,我们了解认识一下。 在systemveriog中,存在三种并发执行语句,分别是fork..join,fork...join_any和fork..join_none,其中只有fork...join_none不…

OSPF路由协议(红茶三杯CCNA)

链路状态路由协议 OSPF&#xff08;开放式最短路径优先&#xff09;Open Shortest Path First 是一种链路状态路由协议&#xff0c;无路由循环&#xff08;全局拓扑&#xff09;&#xff0c;RFC2328 “开放”意味着非私有的 管理型距离&#xff1a;110 OSPF采用SPF算法计算到达…

vmware虚拟机屏幕太小怎么办

虚拟机里面安装Ubuntu有时候屏幕会自动调整&#xff0c;有时会不会自动调整。 当遇到屏幕太小怎么办&#xff1f; 调整Ubuntu屏幕分辨率就可以了。 我不能说不能通过设置虚拟机达到同样的效果&#xff0c; 但是我可以告诉你设置Ubuntu系统的分辨率可以解决这个问题。 具体…

python爬虫(一)_爬虫原理和数据抓取

关于Python爬虫&#xff0c;我们需要学习的有&#xff1a; Python基础语法学习(基础知识)HTML页面的内容抓取(数据抓取)HTML页面的数据提取(数据清洗)Scrapy框架以及scrapy-redis分布式策略(第三方框架)爬虫(Spider)、反爬虫(Anti-Spider)、反反爬虫(Anti-Anti-Spider)之间的斗…

VIS for AI :ConvNetJS

1.简单介绍&#xff1a; ConvNetJS是由斯坦福大学计算机科学系的Andrej Karpathy开发的一个深度学习框架&#xff0c;用于在浏览器中运行卷积神经网络&#xff08;ConvNet&#xff09;。ConvNetJS可以帮助开发人员在客户端&#xff08;浏览器&#xff09;上进行深度学习任务&a…

NLP From Scratch: 生成名称与字符级RNN

NLP From Scratch: 生成名称与字符级RNN 这是我们关于“NLP From Scratch”的三个教程中的第二个。 在<cite>第一个教程< / intermediate / char_rnn_classification_tutorial ></cite> 中&#xff0c;我们使用了 RNN 将名称分类为来源语言。 这次&#xff…

算法竞赛入门【码蹄集新手村600题】(MT1060-1080)

算法竞赛入门【码蹄集新手村600题】(MT1060-1080&#xff09; 目录MT1061 圆锥体的体积MT1062 圆锥体表面积MT1063 立方体的体积MT1064 立方体的表面积MT1065 长方体的表面积MT1066 射线MT1067 线段MT1068 直线切平面MT1069 圆切平面MT1070 随机数的游戏MT1071 计算表达式的值M…

[vue] 新项目配置整理(没写完,回头有空继续)

省流版(vue2项目)&#xff1a; 脚手架&#xff1a;vue create xxxx vuex&#xff1a;npm i vuex3 router : npm install vue-router3 vue cli创建项目 vue create xxxx(项目名称) 添加基本配置 module.exports {lintOnSave: false, // 关闭eslint检查publicPath:./, //…

Python(四十七)列表对象的创建

❤️ 专栏简介&#xff1a;本专栏记录了我个人从零开始学习Python编程的过程。在这个专栏中&#xff0c;我将分享我在学习Python的过程中的学习笔记、学习路线以及各个知识点。 ☀️ 专栏适用人群 &#xff1a;本专栏适用于希望学习Python编程的初学者和有一定编程基础的人。无…

JiaYu说:如何做好IT类的技术面试?

IT类的技术面试 面试IT公司的小技巧IT技术面试常见的问题嵌入式技术面试嵌入式技术面试常见的问题嵌入式软件/硬件面试题 JiaYu归属嵌入式行业&#xff0c;所以这里只是以普通程序员的角度去分析技术面试的技巧 当然&#xff0c;也对嵌入式技术面试做了小总结&#xff0c;友友们…

python 源码中 PyId_stdout 如何定义的

python 源代码中遇到一个变量名 PyId_stdout&#xff0c;搜不到在哪里定义的&#xff0c;如下只能搜到引用的位置&#xff08;python3.8.10&#xff09;&#xff1a; 找了半天发现是用宏来构造的声明语句&#xff1a; // filepath: Include/cpython/object.h typedef struct …

MIPI D-PHY 2.1协议(学习笔记)

1~3 简介/术语/参考文档 这三章属于介绍性内容&#xff0c;包括缩略语等名词术语解释内容&#xff0c;不再赘述。 直接进入以下正文部分 4 D-PHY概述 D-PHY描述了一种Source同步、高速、低功耗、低成本的PHY&#xff0c;特别适用于移动应用。这个D-PHY规范主要是为了将相机…

持续部署CICD

目录 &#xff08;1&#xff09;CICD的开展场景 &#xff08;2&#xff09;项目实际应用 CICD 是持续集成&#xff08;Continuous Integration&#xff09;和持续部署&#xff08;Continuous Deployment&#xff09;简称。指在研发过程中自动执行一系列脚本来降低开发引入 bug…

【雕爷学编程】Arduino动手做(175)---机智云ESP8266开发板模块

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

【KVC补充 Objective-C语言】

一、KVC补充 好,那么接下来,再给大家说一下这个KVC 1.首先我们说,这个KVC,就是指的什么 key value coding 吧 全称就是叫做(Key Value Coding),这是它的全称 那么,你在帮助文档里面搜的时候,你就搜key-value coding 是不是这个啊,key-value coding 然后点击,进…

HashMap中hash方法的作用(详解)

首先&#xff0c;hash方法用来干什么&#xff1f; 在搞清楚原理之前&#xff0c;我们先站在巨人的肩膀浅浅了解一下hash方法的本质作用。 实质上&#xff0c;它的作用很朴素&#xff0c;就是用key值通过某种方式计算出一个hash码 而且这个hash码我们后面要用来计算key存在底…