什么是MVVM
- MVVM是是Model-View-ViewModel的缩写,Model代表数据模型,定义数据操作的业务逻辑,View代表视图层,负责将数据模型渲染到页面上,ViewModel通过双向绑定把View和Model进行同步交互,不需要手动操作DOM的一种设计思想。
MVVM的实现过程
需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter
这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
前提须知
- Object.defineProperty()
- 观察者模式
开始封装
示例
let vm=new MVVM({
el:'#app',
data:{
message:{
a:'hello',
b:'world'
}
}
})
以上代码 接收一个对象 包括el节点和data数据
MVVM.js
- 初始模板和数据
- 整合Observer、Compile和Watcher三者 实现数据的响应式变化
class MVVM{
constructor(options){
this.$el=options.el;
this.$data=options.data;
if(this.$el){//如果有要编译的模板 就开始编译
//数据劫持 就是把数据对象的所有属性,改成带set和get方法的,
//当然 ,把需要劫持的数据传进去
new Observe(thia.$data);
//模板解析
//用数据和元素进行编译
//当然要把模板el传进去,还传入当前实例,方便获取真实数据使用
new Compile(this.$el,this);
}
}
}
observer.js 数据劫持
- 基于Object.defineProperty(data,key,{get(){},set(){}});
- 需要把data所有属性都进行监听,包括子属性(递归)
- 作用是当数据变化的时候触发set,可以再set中设置监听,触发重新编译,使视图更新
class Observer{
constructor(data){
this.data=data;
this.observe(this.data);
}
observe(data){//循环所有的属性添加get set
if(!data || typeof data!='object'){//data不是一个对象就不要往下进行了
return;
}
//要将数据一一劫持 先获取到data的key和value
Object.keys(data).forEach((key)=>{
this.definedReactive(data,key,data[key]);
this.observe(data[key]);//递归调用,给所有的属性都加get set
})
}
definedReactive(data,key,value){
Object.defineProperty(data,key,{
get(){
return value;
// todo。。。
},
set(newValue){
value=newValue;
//这里数据值更新 todo。。。
}
})
}
}
compile编译模板
- 在模板中,为避免node节点重复操作 使用fragment文档碎片
- 筛选文本节点(处理{{}}),和元素节点(处理v-指令)
- 拿到指令中对应data的真实值,进行替换,换成真实数据
- 在把fragment插回到模板中
class Compile{
constructor(el,vm){
this.el=this.isElement(el)?el:document.querySelector(el);
this.vm=vm;
if(this.el){
//如果能够取到元素 我们才开始编译
//1.把el中的节点都放到fragment中,避免大量操作dom影响性能
this.fragment=this.node2fragment(this.el);
//2.编译=>提取想要的元素节点 v-model 和文本节点{{}}
thnis.compile(this.fragment);
//3.fragent插入回模板中
this.el.appendChild(this.fragment);
}
}
//一些辅助的方法
isElement(el){//判断元素节点
return el.nodeType==1;
}
isDirective(attr){//以v-开头的属性就是指令
return attr.startsWith('v-');
}
//核心的方法
node2fragment(el){
let fragment=document.createDocumentFragment();
let firstChild;
while(firstNode=el.firstChild){//把所有真实的dom节点都放到fragment中去
fragment.appendChild(firstChild);
//如果将文档中的节点添加到文档碎片中,就会从文档树中移除该节点,
//也不会在浏览器中再看到该节点
}
return fragment;
}
compile(fragment){
//遍历所有的子节点判断节点类型
let childNodes=fragment.childNodes;
Array.from(childNodes).forEach(node=>{
if(this.isElementNode(node)){//元素节点
//如果是元素节点,深入判断他的子元素节点
this.complie(node);//递归判断
this.compileElement(node);
//这里需要编译元素
//提取元素上的v-model属性
}else{//文本节点
//这里需要编译为文本
this.compileText(node)
}
})
}
compileElement(node){//编译元素
let attrs=node.attributes;//取出当前节点的所有属性(类数组)
Array.from(attrs).forEach((attr)=>{
let attrName=attr.name;// 取到属性名
if(this.isDirective(attrName)){//如果这个属性是一个指令
//拿到属性值这个变量,替换真实数据
let expr=attr.value;
//如果是 message.a 需要变成data.message.a
//把指令后边的属性expr 替换成真实的data中的数据 绑定到dom上
//node this.vm.$data expr
let [,type]=attrName.split('-');//解构赋值v-model-->model
CompileUtil[type](node,this.vm,expr);
}
})
}
compileText(node){//编译文本 针对{{}}
let expr=node.textContent;//取文本中的内容
//console.log(typeof expr,node)
//console.log(typeof node,node)
//对象类型不能用正则操作,所以用textContent
let reg=/\{\{([^}]+)\}\}/;
if(reg.text(expr)){//说明匹配到了{{}}语法
//处理expr,
//把{{}}就是expr 替换成真实的data中的数据 绑定到dom上
//node this.vm.$data expr
CompileUtil['text'](node,this.vm,expr);
}
}
}
let CompileUtil={
getVal(vm,expr){
expr=expr.split('.');//[message,a];
return expr.reduce((prev,next)=>{
return prev[next];
},vm.$data)
},
getTextVal(vm,expr){
return expr.replace(/\{\{([^}])+\}\}/g,(...arg)=>{
return this.getVal(vm,arg[1])
})
},
text(node,vm,expr){
//expr是带{{}}的 ,需要先去大括号{{message.a}}{{message.b}}==>helloworld
//vm.$data[expr] //vm.$data['message.a']; 取不到数据 应该是vm.$data.message.a
let updateFn=this.updater['textUpdater'];
if(updateFn){
updateFn(node,this.getTextVal(vm,expr))
}
},
model(node,vm,expr){
//vm.$data[expr] //vm.$data['message.a']; 取不到数据 应该是vm.$data.message.a
if(updateFn){
updateFn(node,this.getVal(vm,expr))
}
},
updater:{
//文本更新({{}})
textUpdater(node,value){
node.textContent=value;
},
//指令属性值更新(v-model)
modelUpdata(node,value){
node.value=value;
}
}
}
以上的代码实现了真实数据替换指令中的变量,那么怎么将数据变化和视图更新联系起来,那么就用到了watcher
watcher
- 观察者的目的是给需要变化的元素增减一个观察者,当数据变化的时候执行对应的方法
- 当数据初始化的时候,先保存老的值,此时不调用数据更新,当值发生变化的时候就触发updata重新调用真实数据替换
- 那么watcher要做的事,就是保存老值,和数据变化时候,触发更新
- 获取真实数据需要 vm实例,指令对应的变量expr和数据更新后要执行的回调
class Watcher{
constructor(vm,expr,cb){
this.vm=vm;
this.expr=expr;
this.cb=cb;
//先获取一下老的值
this.value=this.getData();
}
//借用一下获取真实数据的函数
getVal(vm,expr){
expr=expr.split('.');
return expr.reduce((prev,next)=>{
return prev[next];
},vm.$data);
}
getData(){
let value=this.getVal(this.vm,this.expr);//拿到真实的数据
return value;
}
//对外暴露的方法updata
updata(){
let newValue=this.getVal(this.vm,this.expr);//当updata执行的时候,获取新的真实的值
let oldValue=this.value;//获取老的值;
if(newValue!=oldValue){//如果执行updata的时候新的值和老的值不相等就调用回调函数
this.cb(newValue)///调用对应watch的callback
}
}
}
dep----关于订阅发布
- 当监听data的时候,触发属性的get获取值的时候,应该把监听器watcher进行订阅,拿到老的真实的值
- 当数据变化,属性的set执行应该触发watcher的updata,执行updata的回调,在updata的回调中将真实数据绑定到模板上
- 第一次执行属性的get时,应该是数据第一次绑定到模板,不应该触发数据监听,以后的数据变化才应该进行updata,那么有了一个特殊的做法
class Dep{
constructor(){
//订阅的数组
this.subs=[];
}
addSub(watcher){
this.subs.push(watcher);
}
notify(){
this.subs.forEach((watcher)=>{
watcher.update();
})
}
}
结合订阅发布,实现数据的响应式变化
1.Observer
class Observer{
constructor(data){
this.data=data;
this.observe(data)
}
observe(data){
//要对这个数据监听将原有的属性改成set和get的形式
if(!data || typeof data!=='object'){
return;
}
//要将数据一一劫持 先获取到data的key和value
Object.keys(data).forEach((key)=>{
this.defineReactive(data,key,data[key])
this.observe(data[key])//递归劫持
})
}
//定义数据劫持
defineReactive(obj,key,value){
let that=this;
let dep=new Dep();//每个变化的数据 都会对应一个数组,这个数组是存放所有更新的操作
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
//如果有target说明watcher进行了初始化,增加监听
Dep.target&&dep.addSub(Dep.target)
return value;
},
set(newValue){//当给data属性中设置值的时候 更改获取的属性的值
if(newValue!=value){
that.observe(newValue)//劫持新的newValue(如果是对象 继续劫持)
value=newValue;
dep.notify();//通知所有人 数据更新了,触发watcher的updata函数,执行真实数据的绑定
}
}
})
}
}
- Compile
class Compile{
constructor(el,vm){
this.el=this.isElementNode(el)?el:document.querySelector(el);
this.vm=vm;
if(this.el){
//如果能够取到元素 我们才开始编译
//1.先把这些真实的DOM移入到内存中 fragment
let fragment=this.node2fragment(this.el);
//2.编译=>提取想要的元素节点 v-model 和文本节点{{}}
this.complie(fragment);
//把编译好的fragment塞回到页面中去
this.el.appendChild(fragment);
}
}
/*专门写一些辅助的方法*/
isElementNode(node){//判断是不是元素节点
return node.nodeType==1;
}
isDirective(attrName){
return attrName.startsWith('v-');
}
/*核心的方法*/
node2fragment(el){//需要将el中的所有节点都放到文档碎片中去
let fragment=document.createDocumentFragment();
let firstChild;
while(firstChild=el.firstChild){//把所有真实的dom节点都放到fragment中去
fragment.appendChild(firstChild);//如果将文档中的节点添加到文档碎片中,就会从文档树中移除该节点,也不会在浏览器中再看到该节点
}
return fragment;
}
complie(fragment){//提取想要的元素节点 v-model 和文本节点{{}}
let childNodes=fragment.childNodes;
Array.from(childNodes).forEach(node=>{
if(this.isElementNode(node)){//元素节点
//如果是元素节点,深入判断他的子元素节点
this.complie(node);//递归判断
this.compileElement(node);
//这里需要编译元素
//提取元素上的v-model属性
}else{//文本节点
//这里需要编译为文本
this.compileText(node)
}
})
}
compileElement(node){//编译元素
//判断当前node元素属性上有没有v-的
let attrs=node.attributes;//取出当前节点的所有属性(类数组)
//console.log(attrs)
Array.from(attrs).forEach((attr)=>{
//判断属性名是不是包含v-的指令
let attrName=attr.name;
if(this.isDirective(attrName)){
//取到真实的值方法node节点中
let expr = attr.value;
//把指令后边的属性expr 替换成真实的data中的数据 绑定到dom上
//node this.vm.$data expr
let [,type]=attrName.split('-');//解构赋值
CompileUtil[type](node,this.vm,expr);
}
})
}
compileText(node){//编译文本 针对{{}}
let expr=node.textContent;//取文本中的内容(字符串)
//console.log(typeof expr,node)
//console.log(typeof node,node)//对象类型不能用正则操作
let reg=/\{\{([^}]+)\}\}/g;
if(reg.test(expr)){//说明匹配到了{{}}语法
//把{{}}就是expr 替换成真实的data中的数据 绑定到dom上
//node this.vm.$data expr
CompileUtil['text'](node,this.vm,expr);
}
}
}
CompileUtil={
getVal(vm,expr){
expr=expr.split('.');//[message,a];
return expr.reduce((prev,next)=>{
return prev[next];
},vm.$data);
},
getTextVal(vm,expr){
return expr.replace(/\{\{([^}]+)\}\}/g,(...arg)=>{
return this.getVal(vm,arg[1])
})
},
setVal(vm,expr,value){
expr=expr.split('.');//[message,a];
return expr.reduce((prev,next,currentIndex)=>{
if(currentIndex===expr.length-1){
return prev[next]=value;
}
return prev[next];
},vm.$data);
},
text(node,vm,expr){//文本处理
let updateFn=this.updater['textUpdater'];
//expr是带{{}}的 ,需要先去大括号{{message.a}}{{message.b}}==>helloworld
//vm.$data[expr] //vm.$data['message.a']; 取不到数据 应该是vm.$data.message.a
let val=this.getTextVal(vm,expr)
expr.replace(/\{\{([^}]+)\}\}/g,(...arg)=>{
new Watcher(vm,arg[1],(newValue)=>{
//如果数据变化了,文本节点需要重新获取依赖的属性更新文本中的内容
updateFn&&updateFn(node,this.getTextVal(vm,expr));
})
})
updateFn&&updateFn(node,val);
},
model(node,vm,expr){//输入框处理
let updateFn=this.updater['modelUpdater'];
//vm.$data[expr] //vm.$data['message.a']; 取不到数据 应该是vm.$data.message.a
//这里应该加一个监控,数据变化了 应该调用watch的callback(这里只是记录原始的值 watcher的updata没有执行,只有属性的set执行的时候,才会执行cb回调,重新进行真实数据绑定)
new Watcher(vm,expr,(newValue)=>{
//当值变化后,会调用cb将新值传递过来
updateFn&&updateFn(node,this.getVal(vm,expr))
})
node.addEventListener('input',(e)=>{
let newValue=e.target.value;
this.setVal(vm,expr,newValue)
})
updateFn&&updateFn(node,this.getVal(vm,expr))
},
updater:{
//文本更新({{}})
textUpdater(node,value){
node.textContent=value
},
//指令属性值更新(v-model)
modelUpdater(node,value){
node.value=value;
}
}
}
3.Watcher
class Watcher{
constructor(vm,expr,cb){
this.vm=vm;
this.expr=expr;
this.cb=cb;
//先获取一下老的值
this.value=this.get();
}
getVal(vm,expr){
expr=expr.split('.');//[message,a];
return expr.reduce((prev,next)=>{
return prev[next];
},vm.$data);
}
get(){
Dep.target=this;
let value=this.getVal(this.vm,this.expr);
Dep.target=null;
return value;
}
//对外暴露的方法
update(){
let newValue=this.getVal(this.vm,this.expr);
let oldValue=this.value;
if(newValue!=oldValue){
this.cb(newValue)///调用对应watch的callback
}
}
}
4.dep
class Dep{
constructor(){
//订阅的数组
this.subs=[];
}
addSub(watcher){
this.subs.push(watcher);
}
notify(){
this.subs.forEach((watcher)=>{
watcher.update();
})
}
}
5.MVVM
class MVVM{
constructor(options){
//一上来 先把可用的东西挂载在实例上
this.$el=options.el;
this.$data=options.data;
//如果有要编译的模板 就开始编译
if(this.$el){
//数据劫持 就是把数据对象的所有属性,改成带set和get方法的
new Observer(this.$data);
//用数据和元素进行编译
new Compile(this.$el,this);//并且把mvvm实例传进去,方便编译使用
}
}
}
以上实现一个数据响应式变化的MVVM框架,目前拥有v-model、{{}}指令,后期可增加其他指令的功能。