index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="message.name">
{{message.name}} {{message.oinfo}}
<div>
<input type="password" v-model="message.pwd">
{{message.pwd}}
</div>
{{message.obj.name}}
</div>
<script src="./Dep.js"></script>
<script src="./Watcher.js"></script>
<script src="./Compile.js"></script>
<script src="./Observer.js"></script>
<script src="./MVVM.js"></script>
<script>
var vm=new MVVM({
el:"#app",
data:{
message:{
name:'小明',
pwd:'12345',
oinfo:'其它信息',
obj:{
name:'小红'
}
}
}
})
</script>
</body>
</html>
MVVM.js
class MVVM{
constructor(options){
this.$el=options.el;
this.$data=options.data;
new Observer(this.$data)
this.proxyData(this,this.$data)
new Compile(this,this.$el)
}
//数据代理,可以直接从vm上获取vm.$data的属性
proxyData(obj,data){
Object.keys(data).forEach(key=>{
Object.defineProperty(obj,key,{
get(){
return data[key]
},
set(newValue){
data[key]=newValue
}
})
})
}
}
Observer.js
class Observer{
constructor(data){
this.Observe(data)
}
Observe(data){
if(!data || typeof data !=='object'){
return
}
Object.keys(data).forEach(key=>{
this.DefinedReactive(data,key,data[key])
//属性值可能是对象,继续递归处理
this.Observe(data[key]);
})
}
DefinedReactive(data,key,value){
let that=this;
let dep=new Dep()
//这里的value是之前的值
Object.defineProperty(data,key,{
get(){
// console.log(`获取了${data}[${key}]的值`);
//假设修改了message.name,那么就会更新界面中用到了mesage.name的地方
//假设修改了vm.$data.message,那么就会更新界面中用到了vm.$data.message的地方,包含message.name
//vm.$data.message中的get和set中的dep存在了可以更新用到vm.$data.message的地方的节点,比如message.name
Dep.target&&dep.addSub(Dep.target)
return value
},
set(newValue){
if(value!==newValue){
console.log(value,newValue)
//这样赋值会导致无限递归
// data[key]=newValue;
//这里很巧妙,使用了闭包,get和set都是用的闭包中的value。
//也就是说对象中的value没有被修改,只是用的数据劫持,然后用的是闭包中的value!
//这样做,可以避免无限递归的问题了
value=newValue
//如果赋值的是对象,继续劫持
that.Observe(newValue)
dep.notify()
}
}
})
}
}
Compile.js
class Compile{
constructor(vm,el){
this.el=this.isElementNode(el)?el:document.querySelector(el);
this.vm=vm;
if(this.el){
let fragment=this.NodeToFragement(this.el);
this.Compile(fragment)
//将fragment放到#app中
this.el.appendChild(fragment)
}
}
isElementNode(node){
return node.nodeType===1
}
NodeToFragement(node){
//将页面中的节点放到fragment中,方便以后取出
let fragment=document.createDocumentFragment()
Array.from(node.childNodes).forEach(child=>{
//注意:appendChild具有移动的效果,也就是说页面中的节点是真的被移走了。。。
fragment.appendChild(child)
})
return fragment
}
CompileElement(node){
let attrs=node.attributes;
Array.from(attrs).forEach(attr=>{
if(attr.name.includes('v-')){
let [,type]=attr.name.split('-');
let expr=attr.value;
compileUtil[type](this.vm,expr,node)
}
})
}
CompileText(node){
let expr=node.textContent;
let reg=/\{\{([^}]+)\}\}/g
//判断是否含有{{}}
if(reg.test(expr)){
compileUtil['text'](this.vm,expr,node)
}
}
Compile(node){
Array.from(node.childNodes).forEach(childNode=>{
//是元素节点
if(this.isElementNode(childNode)){
this.CompileElement(childNode)
this.Compile(childNode)
//是文本节点
}else{
// console.log('是文本节点');
this.CompileText(childNode)
}
})
}
}
compileUtil={
getVal(vm,expr){
//将类似message.name的表达式,用split分隔为数组
let names=expr.split('.')
return names.reduce((prev,next)=>{
return prev[next]
},vm.$data)
},
getTextVal(vm,expr){
let reg=/\{\{([^}]+)\}\}/g
//可能含有多个表达式,{{message.name}} - {{message.pwd}}
return expr.replace(reg,(...args)=>{
return this.getVal(vm,args[1])
})
},
//设置值
setVal(vm,expr,value){
let names=expr.split('.')
names.reduce((prev,next,index)=>{
if(index===(names.length-1)){
prev[next]=value;
}
return prev[next]
},vm.$data)
},
//处理非指令
text(vm,expr,node){
let reg=/\{\{([^}]+)\}\}/g
expr.replace(reg,(...args)=>{
new Watcher(vm,args[1],()=>{
this.updateText(node,this.getTextVal(vm,expr));
})
})
this.updateText(node,this.getTextVal(vm,expr))
},
//处理v-model指令
model(vm,expr,node){
new Watcher(vm,expr,()=>{
this.updateModel(node,this.getVal(vm,expr))
})
//实现双向绑定!
node.addEventListener('input',(ev)=>{
this.setVal(vm,expr,ev.target.value)
})
this.updateModel(node,this.getVal(vm,expr))
},
//更新input中的value
//根据v-model指令对应的数据,更新node中的value
updateModel(node,value){
node.value=value
},
//更新文本节点中的textContent
//根据node中的{{}}中的数据,更新node中的textContent
updateText(node,value){
node.textContent=value
}
}
Watcher.js
class Watcher{
constructor(vm,expr,cb){
this.vm=vm;
this.expr=expr;
//cb是回调函数
this.cb=cb;
//设置这个以后,执行到下面取值的操作时,触发set和get后
//在get中可以把当前的watcher放到对应get中的dep中了
//为什么我要说对应的get中的dep呢
//因为在栈中有不同的get,get(闭包方法)保存了对dep的引用
Dep.target=this;
//保存旧的值
this.value=this.getVal()
//防止get中的dep重复添加相同的watcher
//也防止了其它的get中的dep也添加了这里的watcher
Dep.target=null;
}
getVal(){
let names=this.expr.split('.')
//假设读取的是message.name
//那么在下面的代码中会读取到vm.$data.message和vm.$data.message.name
//这样会触发两个get,在这两个get中的dep会把当前的watcher添加进去的
//所以以后你修改vm.$data.message和vm.$data.message.name触发对应set方法后,里面的dep会调用notify让这里的watcher调用update方法更新页面的
return names.reduce((prev,next)=>{
return prev[next]
},this.vm.$data)
}
update(){
let newValue=this.getVal()
if(this.value!==newValue){
this.cb()
}
}
}
Dep.js
class Dep{
constructor(){
this.subs=[]
}
addSub(watcher){
this.subs.push(watcher)
}
notify(){
this.subs.forEach(watcher=>{
watcher.update()
})
}
}
问题
这个简易的MVVM,如果给vm中的message直接赋值一个对象的话,界面是可以更新的,但是以后修改message中属性时,界面是无法更新的。之所以产生这个问题,是因为修改vm中message属性(这里用了数据代理,实则访问的是vm. d a t a . m e s s a g e ) 会 触 发 v m . data.message)会触发vm. data.message)会触发vm.data中message属性的get和set,而此时的dep中的subs存放了可以更新界面中使用了带message的表达式的节点的watcher。但是给message重新赋值一个对象后,修改message中的属性时,虽可以触发其set和get,但是其中的dep中的subs数组没有watcher。
上面这个问题在vue中是没有的,希望大家可以帮我解决这个问题。