如何使用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 相关文章推荐
jQuery ajax cache缓存问题
Jul 01 Javascript
js的表单操作 简单计算器
Dec 29 Javascript
jQuery中关于ScrollableGridPlugin.js(固定表头)插件的使用逐步解析
Jul 17 Javascript
浅谈Javascript变量作用域问题
Dec 16 Javascript
浅谈时钟的生成(js手写简洁代码)
Aug 20 Javascript
Bootstrap模态框插入视频的实现代码
Jun 25 Javascript
vue语法之拼接字符串的示例代码
Oct 25 Javascript
js动态添加表格逐行添加、删除、遍历取值的实例代码
Jan 25 Javascript
vue+webpack模拟后台数据的示例代码
Jul 26 Javascript
Element-Ui组件 NavMenu 导航菜单的具体使用
Oct 24 Javascript
JavaScript array常用方法代码实例详解
Sep 02 Javascript
vue导入.md文件的步骤(markdown转HTML)
Dec 31 Vue.js
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
Swoole-1.7.22 版本已发布,修复PHP7相关问题
2015/12/31 PHP
PHP折半(二分)查找算法实例分析
2018/05/12 PHP
Laravel5.7 Eloquent ORM快速入门详解
2019/04/12 PHP
JSCode all of Brower 全局屏蔽网页右键功能 具体实现
2013/06/05 Javascript
JS过滤url参数特殊字符的实现方法
2013/12/24 Javascript
js动态修改整个页面样式达到换肤效果
2014/05/23 Javascript
微信内置浏览器私有接口WeixinJSBridge介绍
2015/05/25 Javascript
jquery实现最简单的滑动菜单效果代码
2015/09/12 Javascript
JS代码实现根据时间变换页面背景效果
2016/06/16 Javascript
JavaScript中闭包之浅析解读(必看篇)
2016/08/25 Javascript
最全面的JS倒计时代码
2016/09/17 Javascript
js日期相关函数dateAdd,dateDiff,dateFormat等介绍
2016/09/24 Javascript
js 数据存储和DOM编程
2017/02/09 Javascript
深入理解Webpack 中路径的配置
2017/06/17 Javascript
Vue如何从1.0迁移到2.0
2017/10/19 Javascript
Vue+webpack项目基础配置教程
2018/02/12 Javascript
Node.js 的 GC 机制详解
2019/06/03 Javascript
jQuery pager.js 插件动态分页功能实例分析
2019/08/02 jQuery
关于layui 实现点击按钮添加一行(方法渲染创建的table)
2019/09/29 Javascript
在vue中高德地图引入和轨迹的绘制的实现
2019/10/11 Javascript
Vue3.x源码调试的实现方法
2019/10/13 Javascript
vue 解决provide和inject响应的问题
2020/11/12 Javascript
python 将md5转为16字节的方法
2018/05/29 Python
django-rest-framework解析请求参数过程详解
2019/07/18 Python
python写程序统计词频的方法
2019/07/29 Python
Python抓包程序mitmproxy安装和使用过程图解
2020/03/02 Python
Python如何用filter函数筛选数据
2020/03/05 Python
python应用Axes3D绘图(批量梯度下降算法)
2020/03/25 Python
tensorflow 2.1.0 安装与实战教程(CASIA FACE v5)
2020/06/30 Python
今天学到的CSS最新技术(与图片背景相关)
2012/12/24 HTML / CSS
有趣的睡衣和礼物:LazyOne
2019/11/27 全球购物
英国领先的鞋类零售商和顶级品牌的官方零售商:Wynsors
2020/02/17 全球购物
澳大利亚著名的纺织品品牌:Canningvale
2020/05/05 全球购物
计算机数据库专业职业生涯规划书
2014/02/08 职场文书
优秀部门获奖感言
2014/02/14 职场文书
电话客服工作职责
2014/07/27 职场文书