如何使用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 写类方式之二
Jul 05 Javascript
JavaScript中的匀速运动和变速(缓冲)运动详细介绍
Nov 11 Javascript
jQuery使用prepend()方法在元素前添加内容用法实例
Mar 26 Javascript
详解JavaScript中getFullYear()方法的使用
Jun 10 Javascript
JS工作中的小贴士之”闭包“与事件委托的”阻止冒泡“
Jun 16 Javascript
javascript基础知识讲解
Jan 11 Javascript
详解vue-router 初始化时做了什么
Jun 11 Javascript
解决vue热替换失效的根本原因
Sep 19 Javascript
JavaScript碎片—函数闭包(模拟面向对象)
Mar 13 Javascript
使用node搭建自动发图文微博机器人的方法
Mar 22 Javascript
详解微信小程序网络请求接口封装实例
May 02 Javascript
利用layer实现表单完美验证的方法
Sep 26 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
thinkPHP引入类的方法详解
2016/12/08 PHP
在js(jquery)中获得文本框焦点和失去焦点的方法
2012/12/04 Javascript
jQuery contains过滤器实现精确匹配使用方法
2013/04/12 Javascript
利用js的Node遍历找到repeater的一个字段实例介绍
2013/04/25 Javascript
实现图片预加载的三大方法及优缺点分析
2014/11/19 Javascript
JavaScript中判断函数、变量是否存在
2015/06/10 Javascript
Bootstrap的fileinput插件实现多文件上传的方法
2016/09/05 Javascript
vue中动态绑定表单元素的属性方法
2018/02/23 Javascript
mpvue小程序仿qq左滑置顶删除组件
2018/08/03 Javascript
vue实现百度下拉列表交互操作示例
2019/03/12 Javascript
使用express来代理服务的方法
2019/06/21 Javascript
jquery添加div实现消息聊天框
2020/02/08 jQuery
vue实现瀑布流组件滑动加载更多
2020/03/10 Javascript
python实现将html表格转换成CSV文件的方法
2015/06/28 Python
Windows下Eclipse+PyDev配置Python+PyQt4开发环境
2016/05/17 Python
Python AES加密实例解析
2018/01/18 Python
Python Cookie 读取和保存方法
2018/12/28 Python
[机器视觉]使用python自动识别验证码详解
2019/05/16 Python
face++与python实现人脸识别签到(考勤)功能
2019/08/28 Python
Python编程快速上手——疯狂填词程序实现方法分析
2020/02/29 Python
Python实现列表中非负数保留,负数转化为指定的数值方式
2020/06/04 Python
C++和python实现阿姆斯特朗数字查找实例代码
2020/12/07 Python
浅谈css3中的渐进增强和优雅降级
2017/12/01 HTML / CSS
css3 实现滚动条美化效果的实例代码
2021/01/06 HTML / CSS
Europcar西班牙:全球汽车租赁领域的领导者
2018/09/17 全球购物
诗普兰迪官方网站:Splendid
2018/09/18 全球购物
社会学专业学生职业规划书
2014/02/07 职场文书
求职信怎么写
2014/05/23 职场文书
怀孕辞职信怎么写
2015/02/28 职场文书
2015年人力资源部工作总结
2015/04/30 职场文书
2016年企业安全生产月活动总结
2016/04/06 职场文书
详解JavaScript中Arguments对象用途
2021/08/30 Javascript
【海涛七七解说】DCG第二周:DK VS 天禄
2022/04/01 DOTA
【D4DJ】美少女DJ企划 动画将于明年冬季开播第2季
2022/04/11 日漫
深入理解mysql事务隔离级别和存储引擎
2022/04/12 MySQL
基于docker安装zabbix的详细教程
2022/06/05 Servers