前言

随着前端各大框架的崛起,为我们平时的开发带来了相当的便利,我们不能一直停留在应用层面,今天就自己动手实现一个乞丐版的MVVM小框架

完整代码github地址

效果

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的调用方式

实现步骤

  1. 数据劫持Observe
  2. 数据代理(让Mvvm对象可以处理数据)
  3. 模板编译Compile
  4. 发布订阅
  5. 视图与数据进行关联
  6. 实现双向数据绑定

代码分析

// 定义框架类名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原理

最新文章

  1. Effective Java Second Edition --- Builder Pattern
  2. iOS错误总结(三)
  3. js从0开始构思表情插件
  4. network config
  5. MVC模型
  6. 并查集——HDOJ-1232-畅通工程
  7. java动态代理汇总
  8. Routine Problem(数学)
  9. 如何对ZBrush中面部进行快速布线
  10. 泛型,动态创建List&lt;T&gt; (转摘)
  11. aspcms标签使用经验
  12. MySQL key/value存储方案(转)
  13. 共享池之六:shared pool latch/ library cache latch /lock pin 简介
  14. C语言函数指针基础
  15. 快速启动软件之 Rolan ,你真的会用?
  16. poj 2195 Going Home(最小费用最大流)
  17. UVA 796 Critical Links(无向图求桥)
  18. 常用到的简单命令 Sublime Git NPM WindowsCMD MacTerminal(Unix命令)
  19. windows基于vs2017的opencv安装
  20. happyxiaofan的程序员书单

热门文章

  1. SCOI2016 Day1 简要题解
  2. IT项目管理——《人月神话》读后感
  3. 「HNOI2016」网络 解题报告
  4. noiac64 sort (二分答案)
  5. Spring 官方教程:使用 Restdocs 创建 API 文档
  6. Linux中OBS在Wayland环境下黑屏只显示鼠标的应对措施
  7. 浅谈 WebDriver如何应对不同浏览器
  8. 神经网络中w,b参数的作用(为何需要偏置b的解释)
  9. Vue的简单入门
  10. R语言修改标题、坐标轴刻度、坐标轴名称的大小(cex.axis、cex.lab、cex.main函数)