JavaScript 实现一个简单的MVVM前端框架(ES6语法)
前言
随着前端各大框架的崛起,为我们平时的开发带来了相当的便利,我们不能一直停留在应用层面,今天就自己动手实现一个乞丐版的MVVM小框架
效果
html代码
<div id="app">
<p>{{a}}</p>
<p>{{b.b}}</p>
<input type="text" v-model="a">
</div>
js调用代码
const vm = new Mvvm({
el: '#app',
data: {
a: 1,
b: { b : 2 }
}
})
基本是模仿vue的调用方式
实现步骤
- 数据劫持Observe
- 数据代理(让Mvvm对象可以处理数据)
- 模板编译Compile
- 发布订阅
- 视图与数据进行关联
- 实现双向数据绑定
代码分析
// 定义框架类名Mvvm,我们则可以直接实例化new Mvvm() 来调用
class Mvvm {
constructor(options){
/**
* options 则是前台传来的数据
* {
el: '#app',
data: {
a: 1,
b: { b : 2 }
}
}
*/
const {el,data} = options;
this._data = data;
Observe.observeData(data); // 通过该函数把所有前台传来的data中的数据劫持
this.mount(data); // 把所有的data数据代理到this,也就是Mvvm对象上
Mvvm.compile(el,this); // 解析模板数据,也就是解析HTML中的{{a}} {{b.b}}
}
// 把data中的数据挂载到this上
mount(data){
// 遍历data数据 通过defineProperty进行重新创建属性到this上
for(let key in data){
Object.defineProperty(this,key,{
enumerable:true, // 可枚举
get(){
return this._data[key];
},
set(newVal){
this._data[key] = newVal;
}
})
}
}
// 解析模板功能
static compile(el,_that){
new Compile(el,_that);
}
}
// 对数据进行劫持
class Observe{
constructor(data){
this.deepObserve(data);
}
deepObserve(data){
let dep = new Dep(); // 创建一个可观察对象
for(let key in data){
let value = data[key];
Observe.observeData(value); // 递归调用数据劫持方法
this.mount(data,key,value,dep); // 数据劫持主体方法
}
}
mount(data,key,value,dep){
// 其实就是把data中的数据一层层递归的通过defineProperty方式创建
Object.defineProperty(data,key,{
enumerable:true,
get(){
Dep.target && dep.addSub(Dep.target); //Dep.target = watcher 这个存在的时候,添加到可观察对象数组中
return value; // get返回该值
},
set(newVal){
// 当设置值时,新值老值进行比对
if(newVal === value){
return;
}
value = newVal;
Observe.observeData(newVal);// 把后来手动设置的值也劫持了
dep.notify(); // 发布所有的订阅来更新界面数据
}
})
}
static observeData(data){
// 递归的终止条件,这里写的并不完善!! 我们主要目的还是理解mvvm
if(typeof data !== 'object'){
return ;
}
return new Observe(data);
}
}
class Compile{
constructor(el,vm){
// vm = this
vm.$el = document.querySelector(el);
// 创建一个文档片段
let fragment = document.createDocumentFragment();
let child;
while(child = vm.$el.firstChild){
// 不断遍历DOM,添加到文档片段(内存)中
fragment.appendChild(child);
}
// replace是解析HTML的核心函数
this.replace(fragment,this,vm);
// 把更新后的文档片段插入回DOM,达到更新视图的目的
vm.$el.appendChild(fragment);
}
// 解析DOM
replace(fragment,that,vm){
// 循环文档片段中的DOM节点
Array.from(fragment.childNodes).forEach(function (node) {
let text = node.textContent; // 节点值
let reg = /\{\{(.*)\}\}/; // 正则匹配{{}}里面的值
// nodeType === 3 表示文本节点
if(node.nodeType === 3 && reg.test(text)){
let arr = RegExp.$1.split('.'); // RegExp.$1获取到 b.b , 并通过.转换成数组
let val = vm; // val 指针指向 vm对象地址
arr.forEach(function (k) {
val = val[k]; // vm['b'] 可以一层层取到值
});
// 给这个node创建一个watcher对象,用于后期视图动态更新使用
new Watcher(vm,RegExp.$1,function (newVal) {
node.textContent = text.replace(reg,newVal);
});
// 更新视图 {{a}} ==> 1
node.textContent = text.replace(reg,val);
}
// 元素节点
if(node.nodeType === 1){
let nodeAttrs = node.attributes; // 获取DOM节点上的属性列表
// 遍历该属性列表
Array.from(nodeAttrs).forEach((attr)=>{
let name = attr.name; // 获取属性名 v-model
let exp = attr.value; // 获取属性值 "a"
if(name.startsWith('v-')){
node.value = vm[exp]; // 实现了把a的值添加到input输入框内
}
// 给该node创建一个watcher对象,用于动态更新视图
new Watcher(vm,exp,function (newVal) {
node.value = newVal; // 更新输入框的值
});
// 输入框添加事件
node.addEventListener('input',function (e) {
// 会调用数据劫持中的set方法,从而触发 dep.notify()发布所有的订阅来更新界面数据
vm[exp] = e.target.value;
},false);
})
}
// 递归解析DOM节点
if(node.childNodes){
that.replace(node,that,vm);
}
});
}
}
// 简单的发布订阅
class Dep{
constructor(){
this.subs = [];
}
addSub(sub){
this.subs.push(sub);
}
notify(){
this.subs.forEach(sub=>{
sub.update();
})
}
}
// Watcher对象,用来跟node的关联起来。把后期需要更新的node变成Watcher对象,存入内存中
class Watcher{
constructor(vm,exp,fn){
this.vm = vm; // this对象
this.exp = exp; // 值
this.fn = fn; // 回调函数
Dep.target = this; // 发布订阅对象Dep,添加一个属性target = this 也是当前watcher
let val = vm;
let arr = exp.split('.');
arr.forEach(function (k) {
val = val[k]; // 这个步骤会循环的调用改对象的get,所以就会把该watcher添加到观察数组中
});
Dep.target = null;
}
// 给每个watcher都加一个update方法,用来发布
update(){
// 通过最新 this对象,去取到最新的值,触发watcher的回调函数,来更新node节点中的数据,以达到更新视图的目的
let val = this.vm;
let arr = this.exp.split('.');
arr.forEach(function (k) {
val = val[k];
});
this.fn(val); // 这个传入的val就是最新计算出来的值
}
}
小结
代码已经全部写了详细的注释,但是可能还是会有难以理解的地方,这个时候多动手练练,使用下可能会让你更加熟悉MVVM原理
JavaScript 实现一个简单的MVVM前端框架(ES6语法)的更多相关文章
- 如何实现一个简单的MVVM框架
接触过web开发的同学想必都接触过MVVM,业界著名的MVVM框架就有AngelaJS.今天闲来无事,决定自己实现一个简单的MVVM框架玩一玩.所谓简单,就是仅仅实现一个骨架,仅表其意,不摹其形. 分 ...
- 230行实现一个简单的MVVM
作者:mirone链接:https://zhuanlan.zhihu.com/p/24451202来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. MVVM这两年在前端届 ...
- Node.js简单介绍并实现一个简单的Web MVC框架
编号:1018时间:2016年6月13日16:06:41功能:Node.js简单介绍并实现一个简单的Web MVC框架URL :https://cnodejs.org/topic/4f16442cca ...
- javascript编写一个简单的编译器(理解抽象语法树AST)
javascript编写一个简单的编译器(理解抽象语法树AST) 编译器 是一种接收一段代码,然后把它转成一些其他一种机制.我们现在来做一个在一张纸上画出一条线,那么我们画出一条线需要定义的条件如下: ...
- 基于vue实现一个简单的MVVM框架(源码分析)
不知不觉接触前端的时间已经过去半年了,越来越发觉对知识的学习不应该只停留在会用的层面,这在我学jQuery的一段时间后便有这样的体会. 虽然jQuery只是一个JS的代码库,只要会一些JS的基本操作学 ...
- 基于gulp编写的一个简单实用的前端开发环境好了,安装完Gulp后,接下来是你大展身手的时候了,在你自己的电脑上面随便哪个地方建一个目录,打开命令行,然后进入创建好的目录里面,开始撸代码,关于生成的json文件请点击这里https://docs.npmjs.com/files/package.json,打开的速度看你的网速了注意:以下是为了演示 ,我建的一个目录结构,你自己可以根据项目需求自己建目
自从Node.js出现以来,基于其的前端开发的工具框架也越来越多了,从Grunt到Gulp再到现在很火的WebPack,所有的这些新的东西的出现都极大的解放了我们在前端领域的开发,作为一个在前端领域里 ...
- 撸一个简单的MVVM例子
我个人以为mvvm框架里面最重要的一点就是VM这部分,它要与Model层建立联系,将Model层转换成可以被View层识别的数据结构:其次也要同View建立联系,将数据及时更新到View层上,并且响应 ...
- 基于gulp编写的一个简单实用的前端开发环境
自从Node.js出现以来,基于其的前端开发的工具框架也越来越多了,从Grunt到Gulp再到现在很火的WebPack,所有的这些新的东西的出现都极大的解放了我们在前端领域的开发,作为一个在前端领域里 ...
- 使用JavaScript实现一个简单的编译器
在前端开发中也会或多或少接触到一些与编译相关的内容,常见的有 将ES6.7代码编译成ES5的代码 将SCSS.LESS代码转换成浏览器支持的CSS代码 通过uglifyjs.uglifycss等工具压 ...
随机推荐
- Eslint相关知识和配置大全
ESLint最初是由Nicholas C. Zakas 于2013年6月创建的开源项目.它的目标是提供一个插件化的javascript代码检测工具. 代码检查是一种静态的分析,常用于寻找有问题的模式或 ...
- 图论杂项细节梳理&模板(虚树,圆方树,仙人掌,欧拉路径,还有。。。)
orzYCB 虚树 %自为风月马前卒巨佬% 用于优化一类树形DP问题. 当状态转移只和树中的某些关键点有关的时候,我们把这些点和它们两两之间的LCA弄出来,以点的祖孙关系连成一棵新的树,这就是虚树. ...
- [POJ 1637] Sightseeing tour(网络流)
题意 (混合图的欧拉回路判定) 给你一个既存在有向边, 又存在无向边的图. 问是否存在欧拉回路. \(N ≤ 200, M ≤ 1000\) 题解 难点在于无向边. 考虑每个点的度数限制. 我们先对无 ...
- iptables(4)规则编写
/etc/sysconfig/iptables # Generated by iptables-save v1.4.7 on Tue Mar 20 15:05:33 2018*filter:INPUT ...
- [NOI2012]美食节(费用流)
题目描述 CZ市为了欢迎全国各地的同学,特地举办了一场盛大的美食节.作为一个喜欢尝鲜的美食客,小M自然不愿意错过这场盛宴.他很快就尝遍了美食节所有的美食.然而,尝鲜的欲望是难以满足的.尽管所有的菜品都 ...
- 汽车控制器LIMPHOME电路设计
摘要:本文介绍汽车控制器上常用的3种LIMPHOME电路设计方法,用于在单片机复位重启期间仍能保证外部输出正确性,确保行车安全. 在电子电气领域,单片机使用非常广泛,单片机的复位重启是设计时必须 ...
- PHP基础学习----字符串操作
1.单引号和双引号的区别 在php中,字符串的定义可以使用英文单引号'',也可以使用英文双引号“”: <?php $str = 'hello'; echo "str is $str&q ...
- 洛谷P2805 植物大战僵尸
题意:给你一张图,每个节点保护若干节点. 当一个节点不被保护的时候,你就可以gay掉它. gay每个节点都有收益(可能为负),求最大总收益. 解:首先发现是一个最大权闭合子图. 把保护关系变成被保护, ...
- 洛谷P1494 小Z的袜子
题意:在[l, r]之中任选两个数,求它们相同的概率. 解: 莫队入门. 概率这个很好搞,就是cnt * (cnt - 1) / 2. 然后发现每次挪指针的时候,某一个cnt会+1或-1.这时候差值就 ...
- NPOI的一些基本操作
1,创建一个Excel //创建一个工作簿 XSSFWorkbook workbook = new XSSFWorkbook(); //创建一个页 ISheet sheet = workbook.Cr ...