如何使用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加强之自定义callback示例
Sep 21 Javascript
JavaScript动态修改网页元素内容的方法
Mar 21 Javascript
jQuery实现连续动画效果实例分析
Oct 09 Javascript
全面解析Bootstrap排版使用方法(文字样式)
Nov 30 Javascript
JavaScript简单获取页面图片原始尺寸的方法
Jun 21 Javascript
详解BootStrap中Affix控件的使用及保持布局的美观的方法
Jul 08 Javascript
jQuery Easyui使用(一)之可折叠面板的布局手风琴菜单
Aug 17 Javascript
关于javascript作用域的常见面试题分享
Jun 18 Javascript
[js高手之路]HTML标签解释成DOM节点的实现方法
Aug 31 Javascript
JavaScript实现的斑马线表格效果【隔行变色】
Sep 18 Javascript
Bootstrap导航菜单点击后无法自动添加active的处理方法
Aug 10 Javascript
layui导出所有数据的例子
Sep 10 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
全国FM电台频率大全 - 24 贵州省
2020/03/11 无线电
使用NetBeans + Xdebug调试PHP程序的方法
2011/04/12 PHP
深入探讨:Nginx 502 Bad Gateway错误的解决方法
2013/06/03 PHP
解密ThinkPHP3.1.2版本之独立分组功能应用
2014/06/19 PHP
Yii基于CActiveForm的Ajax数据验证用法示例
2016/07/14 PHP
在laravel框架中使用model层的方法
2019/10/08 PHP
双击滚屏-常用推荐
2006/11/29 Javascript
用javascript模仿ie的自动完成类似自动完成功的表单
2012/12/12 Javascript
详谈JavaScript内存泄漏
2014/11/14 Javascript
jQuery实现的经典滑动门效果
2015/09/22 Javascript
纯JS前端实现分页代码
2016/06/21 Javascript
JavaScript 是什么意思
2016/09/22 Javascript
解析JavaScript模仿块级作用域
2016/12/29 Javascript
将input框中输入内容显示在相应的div中【三种方法可选】
2017/05/08 Javascript
详解AngularJS ng-class样式切换
2017/06/27 Javascript
Angular2环境搭建具体操作步骤(推荐)
2017/08/04 Javascript
Nodejs 复制文件/文件夹的方法
2017/08/24 NodeJs
jQuery简单实现的HTML页面文本框模糊匹配查询功能完整示例
2018/05/09 jQuery
element-ui中select组件绑定值改变,触发change事件方法
2018/08/24 Javascript
vue-router结合vuex实现用户权限控制功能
2019/11/14 Javascript
vue(2.x,3.0)配置跨域代理
2019/11/27 Javascript
微信小程序实现菜单左右联动
2020/05/19 Javascript
Python图像处理之颜色的定义与使用分析
2019/01/03 Python
浅谈python函数调用返回两个或多个变量的方法
2019/01/23 Python
对Pycharm创建py文件时自定义头部模板的方法详解
2019/02/12 Python
Python函数参数匹配模型通用规则keyword-only参数详解
2019/06/10 Python
bluepy 一款python封装的BLE利器简单介绍
2019/06/25 Python
python3 tcp的粘包现象和解决办法解析
2019/12/09 Python
CSS3不透明度实例讲解
2016/04/26 HTML / CSS
浅谈html5之sse服务器发送事件EventSource介绍
2017/08/28 HTML / CSS
SQL Server的固定数据库角色都有哪些?对应的服务器权限有哪些?
2013/05/18 面试题
面料业务员岗位职责
2013/12/26 职场文书
大学学雷锋活动总结
2014/06/26 职场文书
工厂仓管员岗位职责范本
2014/07/17 职场文书
python数据分析之用sklearn预测糖尿病
2021/04/22 Python
uni-app 微信小程序授权登录的实现步骤
2022/02/18 Javascript