如何使用proxy实现一个简单完整的MVVM库的示例代码

前言

MVVM 是当前时代前端日常业务开发中的必备模式(相关框架如reactvueangular 等), 使用 MVVM 可以将开发者的精力更专注于业务上的逻辑,而不需要关心如何操作 dom。虽然现在都 9012 年了,mvvm 相关原理的介绍已经烂大街了,但出于学习基础知识的目的(使用 proxy 实现的 vue3.0 还在开发中), 在参考了之前 vue.js 的整体思路之后,自己动手实现了一个简易的通过 proxy 实现的 mvvm

本项目代码已经开源在github,项目正在持续完善中,欢迎交流学习,喜欢请点个 star 吧!

最终效果

<html>
 <body>
  <div id="app">
   <div>{{title}}</div>
  </div>
 </body>
</html>
import MVVM from '@fe_korey/mvvm';
new MVVM({
 view: document.getElementById('app'),
 model: {
  title: 'hello mvvm!'
 },
 mounted() {
  console.log('主程编译完成,欢迎使用MVVM!');
 }
});

结构概览

  • Complier 模块实现解析、收集指令,并初始化视图
  • Observer 模块实现了数据的监听,包括添加订阅者和通知订阅者
  • Parser 模块实现解析指令,提供该指令的更新视图的更新方法
  • Watcher 模块实现建立指令与数据的关联
  • Dep 模块实现一个订阅中心,负责收集,触发数据模型各值的订阅列表

流程为:Complier收集编译好指令后,根据指令不同选择不同的Parser,根据ParserWatcher中订阅数据的变化并更新初始视图。Observer监听数据变化然后通知给 WatcherWatcher 再将变化结果通知给对应Parser里的 update 刷新函数进行视图的刷新。

模块详解

Complier

将整个数据模型 data 传入Observer模块进行数据监听

this.$data = new Observer(option.model).getData();

循环遍历整个 dom,对每个 dom 元素的所有指令进行扫描提取

function collectDir(element) {
 const children = element.childNodes;
 const childrenLen = children.length;

 for (let i = 0; i < childrenLen; i++) {
  const node = children[i];
  const nodeType = node.nodeType;

  if (nodeType !== 1 && nodeType !== 3) {
   continue;
  }
  if (hasDirective(node)) {
   this.$queue.push(node);
  }
  if (node.hasChildNodes() && !hasLateCompileChilds(node)) {
   collectDir(element);
  }
 }
}

对每个指令进行编译,选择对应的解析器Parser

const parser = this.selectParsers({ node, dirName, dirValue, cs: this });

将得到的解析器Parser传入Watcher,并初始化该 dom 节点的视图

const watcher = new Watcher(parser);
parser.update({ newVal: watcher.value });

所有指令解析完毕后,触发 MVVM 编译完成回调$mounted()

this.$mounted();

使用文档碎片document.createDocumentFragment()来代替真实 dom 节点片段,待所有指令编译完成后,再将文档碎片追加回真实 dom 节点

let child;
const fragment = document.createDocumentFragment();
while ((child = this.$element.firstChild)) {
 fragment.appendChild(child);
}
//解析完后
this.$element.appendChild(fragment);
delete $fragment;

Parser

Complier模块编译后的指令,选择不同听解析器解析,目前包括ClassParser,DisplayParser,ForParser,IfParser,StyleParser,TextParser,ModelParser,OnParser,OtherParser等解析模块。

switch (name) {
 case 'text':
  parser = new TextParser({ node, dirValue, cs });
  break;
 case 'style':
  parser = new StyleParser({ node, dirValue, cs });
  break;
 case 'class':
  parser = new ClassParser({ node, dirValue, cs });
  break;
 case 'for':
  parser = new ForParser({ node, dirValue, cs });
  break;
 case 'on':
  parser = new OnParser({ node, dirName, dirValue, cs });
  break;
 case 'display':
  parser = new DisplayParser({ node, dirName, dirValue, cs });
  break;
 case 'if':
  parser = new IfParser({ node, dirValue, cs });
  break;
 case 'model':
  parser = new ModelParser({ node, dirValue, cs });
  break;
 default:
  parser = new OtherParser({ node, dirName, dirValue, cs });
}

不同的解析器提供不同的视图刷新函数update(),通过update更新dom视图

//text.js
function update(newVal) {
 this.el.textContent = _toString(newVal);
}

OnParser 解析事件绑定,与数据模型中的 methods字段对应

//详见 https://github.com/zhaoky/mvvm/blob/master/src/core/parser/on.ts
el.addEventListener(handlerType, e => {
 handlerFn(scope, e);
});

ForParser 解析数组

详见 https://github.com/zhaoky/mvvm/blob/master/src/core/parser/for.ts

ModelParser 解析双向绑定,目前支持input[text/password] & textarea,input[radio],input[checkbox],select四种情况的双向绑定,双绑原理:

数据变化更新表单:跟其他指令更新视图一样,通过update方法触发更新表单的value

function update({ newVal }) {
 this.model.el.value = _toString(newVal);
}

表单变化更新数据:监听表单变化事件如input,change,在回调里set数据模型

this.model.el.addEventListener('input', e => {
 model.watcher.set(e.target.value);
});

Observer

MVVM 模型中的核心,一般通过 Object.definePropertygetset 方法进行数据的监听,在 get 里添加订阅者,set 里通知订阅者更新视图。在本项目采用 Proxy 来实现数据监听,好处有三:

Proxy 可以直接监听对象而非属性

Proxy 可以直接监听数组的变化

Proxy 有多达 13 种拦截方法,查阅

而劣势是兼容性问题,且无法通过 polyfill 磨平。查阅兼容性

注意 Proxy 只会监听自身的每一个属性,如果属性是对象,则该对象不会被监听,所以需要递归监听

设置监听后,返回一个 Proxy 替代原数据对象

var proxy = new Proxy(data, {
 get: function(target, key, receiver) {
  //如果满足条件则添加订阅者
  dep.addDep(curWatcher);
  return Reflect.get(target, key, receiver);
 },
 set: function(target, key, value, receiver) {
  //如果满足条件则通知订阅者
  dep.notfiy();
  return Reflect.set(target, key, value, receiver);
 }
});

Watcher

Complier 模块里对每一个解析后的 Parser 进行指令与数据模型直接的绑定,并触发 Observerget 监听,添加订阅者(Watcher

this._getter(this.parser.dirValue)(this.scope || this.parser.cs.$data);

当数据模型变化时,就会触发 -> Observerset 监听 -> Depnotfiy 方法(通知订阅者的所有订阅列表) -> 执行订阅列表所有 Watcherupdate 方法 -> 执行对应 Parserupdate -> 完成更新视图

Watcher 里的 set 方法用于设置双向绑定值,注意访问层级

Dep

  • MVVM 的订阅中心,在这里收集数据模型的每个属性的订阅列表
  • 包含添加订阅者,通知订阅者等方法
  • 本质是一种发布/订阅模式
class Dep {
 constructor() {
  this.dependList = [];
 }
 addDep() {
  this.dependList.push(dep);
 }
 notfiy() {
  this.dependList.forEach(item => {
   item.update();
  });
 }
}

后记

目前该 mvvm 项目只实现了数据绑定视图更新的功能,通过这个简易轮子的实现,对 dom 操作,proxy发布订阅模式等若干基础知识都进行了再次理解,查漏补缺。同时欢迎大家一起探讨交流,后面会继续完善!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 对类Vue的MVVM前端库的实现代码

    MVVM (ModelView ViewModel)是一种基于MVC的设计,开发人员在HTML上写一些Bindings,利用一些指令绑定,就能在Model和ViewModel保持不变的情况下,很方便的将UI设计与业务逻辑分离,从而大大的减少繁琐的DOM操作. 关于实现MVVM,网上实在是太多了,本文为个人总结,结合源码以及一些别人的实现 关于双向绑定 •vue 数据劫持 + 订阅 - 发布 •ng 脏值检查 •backbone.js 订阅-发布(这个没有使用过,并不是主流的用法) 双向绑定,从最

  • JavaScript的MVVM库Vue.js入门学习笔记

    一.v-bind 缩写 <!-- 完整语法 --> <a v-bind:href="url"></a> <!-- 缩写 --> <a :href="url"></a> <!-- 完整语法 --> <button v-bind:disabled="someDynamicCondition">Button</button> <!-- 缩写

  • JavaScript数据绑定实现一个简单的 MVVM 库

    推荐阅读: 实现非常简单的js双向数据绑定 MVVM 是 Web 前端一种非常流行的开发模式,利用 MVVM 可以使我们的代码更专注于处理业务逻辑而不是去关心 DOM 操作.目前著名的 MVVM 框架有 vue, avalon , react 等,这些框架各有千秋,但是实现的思想大致上是相同的:数据绑定 + 视图刷新.出于好奇和一颗愿意折腾的心,我自己也沿着这个方向写了一个最简单的 MVVM 库 ( mvvm.js ),总共 2000 多行代码,指令的命名和用法与 vue 相似,在这里分享一下实

  • 如何使用proxy实现一个简单完整的MVVM库的示例代码

    前言 MVVM 是当前时代前端日常业务开发中的必备模式(相关框架如react,vue,angular 等), 使用 MVVM 可以将开发者的精力更专注于业务上的逻辑,而不需要关心如何操作 dom.虽然现在都 9012 年了,mvvm 相关原理的介绍已经烂大街了,但出于学习基础知识的目的(使用 proxy 实现的 vue3.0 还在开发中), 在参考了之前 vue.js 的整体思路之后,自己动手实现了一个简易的通过 proxy 实现的 mvvm. 本项目代码已经开源在github,项目正在持续完善

  • Python实现一个简单的毕业生信息管理系统的示例代码

    写在前面: 从昨晚的梦里回忆起数据管理的作业: 实现一个自己的选题---- 毕业生信息管理系统,实现学生个人信息基本的增删改查, 我想了想前段时间刚学习的列表,这个简单啊 ,设计一个学生信息列表,然后列表里面再存每个学生详细信息的列表,然后来实现一个基本的增删查改,这个不难啊!直接开始撸代码! 上代码! def Menu():##菜单主界面 print('*'*22) print("* 查看毕业生列表输入: 1 *") print("* 添加毕业生信息输入: 2 *"

  • Java实现一个简单的文件上传案例示例代码

    Java实现一个简单的文件上传案例 实现流程: 1.客户端从硬盘读取文件数据到程序中 2.客户端输出流,写出文件到服务端 3.服务端输出流,读取文件数据到服务端中 4.输出流,写出文件数据到服务器硬盘中 下面上代码 上传单个文件 服务器端 package FileUpload; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.Serve

  • Java实现一个简单的长轮询的示例代码

    目录 分析一下长轮询的实现方式 长轮询与短轮询 配置中心长轮询设计 配置中心长轮询实现 客户端实现 服务端实现 分析一下长轮询的实现方式 现在各大中间件都使用了长轮询的数据交互方式,目前比较流行的例如Nacos的配置中心,RocketMQ Pull(拉模式)消息等,它们都是采用了长轮询方的式实现.就例如Nacos的配置中心,如何做到服务端感知配置变化实时推送给客户端的呢? 长轮询与短轮询 说到长轮询,肯定存在和它相对立的,我们暂且叫它短轮询吧,我们简单介绍一下短轮询: 短轮询也是拉模式.是指不管

  • python实现一个简单的并查集的示例代码

    并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题.常常在使用中以森林来表示. 并查集有三种基本操作,获得根节点,判断两节点是否连通,以及将两不连通的节点相连(相当于将两节点各自的集合合并) 用UnionFind类来表示一个并查集,在构造函数中,初始化一个数组parent,parent[i]表示的含义为,索引为i的节点,它的直接父节点为parent[i].初始化时各个节点都不相连,因此初始化parent[i]=i,让自己成为自己的父节点,从而实现各节点不互连. def __ini

  • 一个简单的JS时间控件示例代码(JS时分秒时间控件)

    自己在网上找了半天没找到只有 "时分秒"的控件, 就自己做了个,发在这里方便有人用到 鼠标点击 后 的效果 SetTime.js 复制代码 代码如下: /**//************************************ 使用说明:* 首先把本控件包含到页面 * <script src="XXX/setTime.js" type="text/javascript"></script>* 控件调用函数:_Set

  • Python实现一个简单三层神经网络的搭建及测试 代码解析

    目录 1.初始化 2.预测 3.训练 4.测试 废话不多说了,直接步入正题,一个完整的神经网络一般由三层构成:输入层,隐藏层(可以有多层)和输出层.本文所构建的神经网络隐藏层只有一层.一个神经网络主要由三部分构成(代码结构上):初始化,训练,和预测.首先我们先来初始化这个神经网络吧! 1.初始化 我们所要初始化的内容包括:神经网络每层上的神经元个数(这个是根据实际问题输入输出而得到的,我们将它设置为一个可自定义量). 不同层间数据互相传送的权重值. 激活函数(模拟自然界的神经元,刺激信号需要达到

  • 一个简单的php MVC留言本实例代码(必看篇)

    摘要 标题上我把这个留言板叫最简单的,其实应该叫最简陋的,因为把全部注意力集中在MVC模式设计和实现上,所以UI方面几乎没有一点修饰. 这个小程序一共包含6个文件,其中index.php是程序入口.post.htm是留言表单.在lib文件夹里Model.View .Controller三个文件分别实现MVC,DataAccess是一个简单的数据库访问类.其实这个程序是国外的一个人写的. PHP代码: /** * 一个用来访问MySQL的类 * 仅仅实现演示所需的基本功能,没有容错等 * 代码未作

  • C# Socket编程实现简单的局域网聊天器的示例代码

    前言 最近在学习C# Socket相关的知识,学习之余,动手做了一个简单的局域网聊天器.有萌生做这个的想法,主要是由于之前家里两台电脑之间想要传输文件十分麻烦,需要借助QQ,微信或者其他第三方应用,基本都要登录,而且可能传输的文件还有大小限制,压缩问题.所以本聊天器的首要目标就是解决这两个问题,做到使用方便(双击启动即用),传文件无限制. 废话不多说,先上图.S-Chat是服务端,C-Chat是客户端,两者除了客户端首次启动后需要设置一下连接的IP地址外,无其他区别.操作与界面都完全相同,对于用

  • Android实现简单的下拉阻尼效应示例代码

    OS的下拉上拉都会出现一个很玄的动态效果.在Android中,虽然可以实现类似的效果,但有点不同的是,如果调用overScrollBy来实现类似的阻尼效应的话,最顶部会出现一片亮的区域,让人感觉不是很爽.所以决定不采用该方法来实现而是改用自定义的方式来实现. 下面是自定义控件的代码部分: public class MyView extends ScrollView { //记录下最开始点击的位置 int initY; //移动的位置 int deltaY; int touchY; //记录第一个

随机推荐