基于ES5defineProperty 实现简单的 Mvvm框架
PREPARE
现阶段前端三大主流框架react
, vue
, angular
都属于 MVVM范畴,即 模型—视图—视图模型
采用数据驱动, 即监听数据改变,渲染view。
核心是监听数据的变更!
其中React
使用的是 diff
算法来实现数据变更检测的;
Angular
则使用的是zone.js
实现数据变更检测;
Vue
则使用Object.defineProperty
, 后期版本则使用Object.Proxy
本文参考Vue
使用 Object.defineProperty
实现数据变更检测, 实现一个简单的 mvvm
框架
INIT
1.下面是一个简单的类Vue
组件的实现方式
-
html
<div id="app"> <h1></h1> <p></p> <p></p> <input type="text" v-model="age" /> </div>
-
javascript
let mvvm = new Mvvm({ el: '#app', data: { song: 2, singer: { a: { b: 1 }, c: 1 }, age: 55 } })
-
首先是一个
Mvvm
类,接受两个参数(后期会加入method等参数):el
和data
。
2. 定义Mvvm
类
function Mvvm(options = {}) {
/*定义类的$option属性,_data私有属性,并将 私有$option.data的引用复制给私有属性_data和局部变 量data
*/
this.$options = options;
let data = this._data = this.$options.data;
/*将data中所有的key设置观察者,增加数据变更的检测*/
observe(data);
/*将data中的所有的key代理到Mvvm实例上,形成mvvm实例树,方便书写*/
for (let key in data) {
Object.defineProperty(this, key, {
configurable: true,
get() {
return this._data[key];
},
set(newVal) {
this._data[key] = newVal;
}
})
}
/*数据如果变更,执行dom渲染*/
new Compile(this.$options.el, this)
}
3.看一下 observe
方法:
function observe(data) {
if (!data || typeof data !== 'object') return;
return new Observe(data);
}
增加了一个data
类型检测机制(实际上为了递归结尾判断),实际上执行 Observe
类的实例化
#####4. Observe
类
function Observe(data) {
/*创建一个消息订阅发布池类,包含一个watcher属性,watcher指向一个Watcher的实例,每个Watcher实例 都是一个订阅者,另一个属性是一个事件池数组events*/
let dep = new Dep();
/*data的每一个键值对循环执行observe方法*/
for (let key in data) {
let val = data[key]
observe(val);
/*对data的每一个key进行数据拦截, 设置get,当创建一个Watcher实例的时候,显示触发get,此时将这个Watcher实例(订阅者)添加到消息订阅发布池的事件池中;设置set 当设置新的值时候,触发 set方法, 如果所设值与原来不相等, 则重新监听新的值得变更, 并触发Watcher实例(订阅者)的notify方法,触发 Watcher实例的回调,更新视图数据*/
Object.defineProperty(data, key, {
configurable: true,
get() {
Dep.watcher && dep.addEvent(Dep.watcher);
return val;
},
set(newVal) {
if (val === newVal) {
return;
}
val = newVal;
observe(newVal);
dep.notify();
}
})
}
}
5.消息订阅发布池Dep
-
类:
function Dep() { this.watcher = null; this.events = []; }
-
实例:
Dep.prototype = { addEvent(event) { this.events.push(event); }, notify() { this.events.forEach(event => event.update()) } }
#####6. 订阅者Watcher
-
类
/*Watcher接收三个参数,第一个是整个mvvm实例树对象, 第二个是html中的插值表达式, fn为设置新值之后的会调*/ function Watcher(vm, exp, fn) { this.fn = fn; this.vm = vm; this.exp = exp; // 将watcher的实例赋值给Dep的watcher属性,方便调用,而不用传参 Dep.watcher = this; // 进行一次取值操作, 显示触发mvvm实例树某个key的get方法,从而将 Dep.watcher 添加到事件池中 // 将如song.a.b以点号分割成数组arr,将mvvm实例树的引用赋值给局部变量val let arr = exp.split('.'); let val = vm; // 循环arr 取song.a.b的值 arr.forEach(key => { val = val[key] }); // 添加完之后,释放 Dep.watcher Dep.watcher = null; }
-
实例
/*Watcher实例的update方法会从mvvm实例树上取出exp所对应的值,并触发fn回调,渲染视图*/ Watcher.prototype.update = function () { let arr = this.exp.split('.'); let val = this.vm; arr.forEach(key => { val = val[key] }); this.fn(val); }
7.Compile类
Compile类用于将所选的el
元素节点 赋值给mvvm
实例树,并转为createDocumentFragment
文档片段, 之后所需要替换的文本节点进行正则匹配并替换,之后将新的文档片段统一添加到el
元素节点中。
关于文档碎片可以 在这里 了解
function Compile(el, vm) {
/*获取元素节点*/
vm.$el = document.querySelector(el);
/*创建文档碎片对象*/
let fragment = document.createDocumentFragment();
/*当vm.$el.firstChild存在时,将vm.$el.firstChild依次加入到文档碎片中*/
while (child = vm.$el.firstChild) {
fragment.appendChild(child);
}
/*对html文档中的插值表达式等进行替换*/
function replace(frag) {
Array.from(frag.childNodes).forEach(node => {
// 插值表达式
let txt = node.textContent;
let reg = /{{(.*?)}}/g;
if (node.nodeType === 1 && reg.test(txt)) {
let arr = RegExp.$1.split('.');
let val = vm;
arr.forEach(key => {
val = val[key]
});
// 执行替换
node.textContent = txt.replace(reg, val).trim();
// 添加监听
new Watcher(vm, RegExp.$1, (newVal) => {
node.textContent = txt.replace(reg, newVal).trim();
})
}
// 双向数据绑定
if(node.nodeType ===1 ) {
let nodeAttr = node.attributes;
// console.log(nodeAttr) Map
Array.from(nodeAttr).forEach(attr => {
let name = attr.name;
let value = attr.value;
if (name.includes('v-')) {
let arr = value.split('.');
let val = vm;
arr.forEach( key => {
val = val[key]
})
console.log(value)
node.value = val
}
new Watcher(vm, value, (newVal) => {
node.value = newVal;
});
node.addEventListener('input', e => {
// 根据传入的绑定值的对象深度值来处理, 如果是单个值,则直接赋值, 如果是多个,则使用eval()函数处理
if(value.split('.').length > 1) {
eval("vm."+ value + "= e.target.value");
} else {
vm[value] = e.target.value
}
})
})
}
if (node.childNodes && node.childNodes.length) {
replace(node)
}
})
}
replace(fragment);
vm.$el.appendChild(fragment);
}
LAODED
以上是一个简单的mvvm框架的实现,当然defineProperty还是有一些问题,比如说对应数组的变更检测是办不到,而Proxy的出现则解决了这类问题,有时间的话,大家可以试试基于Proxy去实现一套简单的mvvm框架。
原文:大专栏 基于ES5defineProperty 实现简单的 Mvvm框架
原文地址:https://www.cnblogs.com/chinatrump/p/11584300.html
- ViewPager轻松完成TabHost效果
- 教你一招 | Python: 函数参数魔法
- 连续多行输入--C++ 中字符串标准输入的学习及实验(续篇)
- 教你一招 | 用Python实现简易可拓展的规则引擎
- 经典算法学习之动态规划
- C++ 隐式类型转换
- IE漏洞调试之CVE-2013-3893
- C++ STL之迭代器注意事项
- 设计3D标签为什么要有一个字符间隙tracking?为什么要重写getPrefferedSize()?画三遍的顺序有讲究
- C++STL之整理算法
- Offset2lib攻击测试:看我如何全面绕过64位Linux的内核防护
- C++ STL之查找算法
- 教你一招 | Python3新特性(一) :字符串
- C++ STL之set的基本操作
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 单细胞转录组高级分析一:多样本合并与批次校正
- 10元最多可喝多少瓶啤酒?(不可借酒+可借酒,swift语言实现)
- 单细胞转录组高级分析二:转录调控网络分析
- 关于数组内元素是否会被改变的思考
- 排序之选择排序实现(swift)
- 去除友盟等第三方SDK失败的解决办法(clang: error: no such file or directory:xx)
- iOS踩坑之多代理模式使用注意
- 单细胞转录组高级分析三:细胞通讯分析
- 单细胞转录组高级分析四:scRNA数据推断CNV
- 0808-7.1.1-如何在CDP7.1.1指定Hive SQL的资源池队列
- iOS多线程之GCD、OperationQueue 对比和实践记录
- singleR的7个数据库文件下载失败的解决方案
- Spring Boot 如何快速集成 Redis 哨兵?
- 一行命令实现成“吨”测试数据的转码
- Jenkins参数化构建与触发