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


Posted in Javascript onSeptember 17, 2019

前言

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 刷新函数进行视图的刷新。

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

模块详解

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发布订阅模式等若干基础知识都进行了再次理解,查漏补缺。同时欢迎大家一起探讨交流,后面会继续完善!

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

Javascript 相关文章推荐
Javascript 篱式条件判断
Aug 22 Javascript
javascript hasFocus使用实例
Jun 29 Javascript
JS实现程序暂停与继续功能代码解读
Oct 10 Javascript
JS对象转换为Jquery对象实现代码
Dec 29 Javascript
js获取select标签的值且兼容IE与firefox
Dec 30 Javascript
bootstrap table操作技巧分享
Feb 15 Javascript
基于JQuery的Ajax方法使用详解
Aug 16 jQuery
原生JS+HTML5实现的可调节写字板功能示例
Aug 30 Javascript
element-ui上传一张图片后隐藏上传按钮功能
May 22 Javascript
基于vue-draggable 实现三级拖动排序效果
Jan 10 Javascript
iview实现图片上传功能
Jun 29 Javascript
Element DateTimePicker日期时间选择器的使用示例
Jul 27 Javascript
ionic+html5+API实现双击返回键退出应用
Sep 17 #Javascript
Vue的属性、方法、生命周期实例代码详解
Sep 17 #Javascript
小程序的上传文件接口的注意要点解析
Sep 17 #Javascript
微信小程序实现蒙版弹出窗功能
Sep 17 #Javascript
kafka调试中遇到Connection to node -1 could not be established. Broker may not be available.
Sep 17 #Javascript
ionic2.0双击返回键退出应用
Sep 17 #Javascript
三步实现ionic3点击退出app程序
Sep 17 #Javascript
You might like
php抓取https的内容的代码
2010/04/06 PHP
PHP实现的构造sql语句类实例
2016/02/03 PHP
PHP如何获取当前主机、域名、网址、路径、端口等参数
2017/06/09 PHP
在textarea文本域中显示HTML代码的方法
2007/03/06 Javascript
jQuery实战之品牌展示列表效果
2011/04/10 Javascript
Prototype源码浅析 Enumerable部分(二)
2012/01/18 Javascript
如何用ajax来创建一个XMLHttpRequest对象
2012/12/10 Javascript
node.js调用C++开发的模块实例
2015/07/03 Javascript
jquery实现超简洁的TAB选项卡效果代码
2015/08/28 Javascript
详细探究ES6之Proxy代理
2016/07/22 Javascript
使用纯JS代码判断字符串中有多少汉字的实现方法(超简单实用)
2016/11/12 Javascript
利用Vue.js实现checkbox的全选反选效果
2017/01/18 Javascript
cocos creator Touch事件应用(触控选择多个子节点的实例)
2017/09/10 Javascript
Vue实现PopupWindow组件详解
2018/04/28 Javascript
vue项目动态设置页面title及是否缓存页面的问题
2018/11/08 Javascript
Vue项目引进ElementUI组件的方法
2018/11/11 Javascript
Vue项目实现简单的权限控制管理功能
2019/07/17 Javascript
原生js实现瀑布流效果
2020/03/09 Javascript
JS表单验证插件之数据与逻辑分离操作实例分析【策略模式】
2020/05/01 Javascript
手把手带你搭建一个node cli的方法示例
2020/08/07 Javascript
vue使用lodop打印控件实现浏览器兼容打印的方法
2021/02/07 Vue.js
python基于ID3思想的决策树
2018/01/03 Python
Python 利用邮件系统完成远程控制电脑的实现(关机、重启等)
2019/11/19 Python
Python 将json序列化后的字符串转换成字典(推荐)
2020/01/06 Python
如何使用Python调整图像大小
2020/09/26 Python
详解如何使用Pytest进行自动化测试
2021/01/14 Python
澳大利亚先进的皮肤和激光诊所购物网站:Soho Skincare
2018/10/15 全球购物
贯彻学习两会心得体会范文
2014/03/17 职场文书
2014年医院十一国庆节活动方案
2014/09/15 职场文书
在宿舍喝酒的检讨书
2014/09/28 职场文书
2014年幼儿园老师工作总结
2014/12/05 职场文书
2015年教研组工作总结
2015/05/04 职场文书
Java Shutdown Hook场景使用及源码分析
2021/06/15 Java/Android
Go web入门Go pongo2模板引擎
2022/05/20 Golang
win10电脑双屏显示一个黑屏怎么办?win10电脑双屏显示一个黑屏解决方法
2022/07/15 数码科技
Win11如何查看显卡型号 Win11查看显卡型号的方法
2022/08/14 数码科技