如何使用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 DOM学习第四章 getElementByTagNames
Feb 19 Javascript
JQuery+Ajax无刷新分页的实例代码
Feb 08 Javascript
AngularJS基础知识笔记之表格
May 10 Javascript
javascript 使用for循环时该注意的问题-附问题总结
Aug 19 Javascript
jquery+正则实现统一的表单验证
Sep 20 Javascript
基于jQuery实现搜索关键字自动匹配功能
Mar 26 Javascript
js表单元素checked、radio被选中的几种方法(详解)
Aug 22 Javascript
十分钟带你快速了解React16新特性
Nov 10 Javascript
vue动画之点击按钮往上渐渐显示出来的实例
Sep 29 Javascript
如何提升vue.js中大型数据的性能
Jun 21 Javascript
layui实现数据分页功能(ajax异步)
Jul 27 Javascript
微信小程序自定义支持图片的弹窗
Dec 21 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使用cookie保存登录用户名的方法
2015/01/26 PHP
php将数组转换成csv格式文件输出的方法
2015/03/14 PHP
在WordPress的后台中添加顶级菜单和子菜单的函数详解
2016/01/11 PHP
解决form中action属性后面?传递参数 获取不到的问题
2017/07/21 PHP
PHP curl批处理及多请求并发实现方法分析
2018/08/15 PHP
PHP实现字母数字混合验证码功能
2019/07/11 PHP
Apache+PHP+MySQL搭建PHP开发环境图文教程
2020/08/06 PHP
jQuery 借助插件Lavalamp实现导航条动态美化效果
2013/09/27 Javascript
jQuery中animate用法实例分析
2015/03/09 Javascript
vue-router2.0 组件之间传参及获取动态参数的方法
2017/11/10 Javascript
nodejs实现的连接MySQL数据库功能示例
2018/01/25 NodeJs
vuejs使用axios异步访问时用get和post的实例讲解
2018/08/09 Javascript
JS html事件冒泡和事件捕获操作示例
2019/05/01 Javascript
nodejs读取图片返回给浏览器显示
2019/07/25 NodeJs
layui 上传文件_批量导入数据UI的方法
2019/09/23 Javascript
详解微信小程序中var、let、const用法与区别
2020/01/11 Javascript
[01:46]TI4西雅图DOTA2前线报道 中国选手抱团调时差
2014/07/08 DOTA
[00:44]TI7不朽珍藏III——军团指挥官不朽展示
2017/07/15 DOTA
解读Django框架中的低层次缓存API
2015/07/24 Python
python中返回矩阵的行列方法
2018/04/04 Python
运行django项目指定IP和端口的方法
2018/05/14 Python
Python操作SQLite/MySQL/LMDB数据库的方法
2019/11/07 Python
python实现二分类的卡方分箱示例
2019/11/22 Python
Python 实现二叉查找树的示例代码
2020/12/21 Python
使用spring mvc+localResizeIMG实现HTML5端图片压缩上传的功能
2016/12/16 HTML / CSS
HTML5、Select下拉框右边加图标的实现代码(增进用户体验)
2017/10/16 HTML / CSS
元旦促销方案
2014/03/15 职场文书
中职招生先进个人材料
2014/08/31 职场文书
团员个人年度总结
2015/02/26 职场文书
个人年终总结怎么写
2015/03/09 职场文书
初中美术教学反思
2016/02/17 职场文书
导游词之西江千户苗寨
2019/12/24 职场文书
Python数据分析之pandas函数详解
2021/04/21 Python
对Golang中的FORM相关字段理解
2021/05/02 Golang
SQL 尚未定义空闲 CPU 条件 - OnIdle 作业计划将不起任何作用
2021/06/30 SQL Server
Promise静态四兄弟实现示例详解
2022/07/07 Javascript