1. 新建一个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>数据劫持实现mvvm(简单模拟vue实现数据驱动视图/以及双向绑定)</title>
</head>
<body>
<div id="app">
{{message}}
<p>{{message}}</p>
<input type="text" a-model='name'>
<span>{{name}}</span>
<button id='btn'>改变message</button>
</div>
<script src="Avue.js"></script>
<script>
let vm=new Avue({
el:"#app",
data:{
message:"消息",
name:"名字",
}
})
let count=1;
let btn=document.getElementById('btn');
btn.addEventListener("click",()=>{
clickHandle()
})
function clickHandle(){
vm.$data.message+=count
}
</script>
</body>
</html>
2. 新建一个Avue.js文件:
/**
* new Avue({
el:"#app",
data:{
message:"消息",
name:"名字"
}
})
* * */
class Avue {
constructor(options) {
this.$options=options; //取名$options是为了避免在data中存在data.options的变量
this.$data=options.data;
this.observer(this.$data);
let root =document.querySelector(options.el);
this.innerHandle(root); //用以将根组件#app的内容取出来,做遍历,然后填充data里面的变量值
}
observer(dataObj){ //遍历options.data对象里面的key,对其通过Object.defineproperty实现数据劫持
Object.keys(dataObj).forEach(key=>{
// if(dataObj[key].constructor==Object){
// this.observer(dataObj[key])
// return
// }
let dep=new Dep(); //实例化一个发布者
this.defineproperty(dataObj,key,dataObj[key],dep);
this.proxyDefineproperty(this,key,dataObj[key],dep)
})
}
proxyDefineproperty(obj,key,val,dep){ //代理defineproperty,实现this.$data.message=this.message
Object.defineProperty(obj,key,{
get(){
if(Dep.target){
dep.addSub(Dep.target)
}
return val
},
set(newVal){
if(val !==newVal){
dep.notify(newVal)
val=newVal;
}
}
})
}
defineproperty(obj,key,val,dep){ //对data对象里面的数据进行数据劫持
if(!obj || obj.constructor !==Object){
return
}
if(obj[key].constructor ===Object){
this.observer(obj[key])
return
}
Object.defineProperty(obj,key,{
get(){
if(Dep.target){
dep.addSub(Dep.target)
}
return val
},
set(newVal){
if(val !==newVal){
dep.notify(newVal)
val=newVal;
}
}
})
}
innerHandle(el){
let childNodes=el.childNodes; //获取root元素的子节点集合(包括文本节点和标签节点)
// console.log(childNodes); //返回:NodeList(7) [text, p, text, input, text, span, text]
// console.log(typeof childNodes); //返回:object
Array.from(childNodes).forEach(node=>{ //childNodes不是数组类型所以要先转换为数组才能遍历
if(node.nodeType===3){ //文本节点
let text=node.textContent;
let reg=/\{\{\s*(\S*)\s*\}\}/; //正则匹配 {{message}}里面的内容
if(reg.test(text)){
node.textContent=this.$data[RegExp.$1]; //RegExp.$1是reg.test(text)匹配到的内容,如message/name
new Watcher(this,RegExp.$1,(newVal)=>{
node.textContent=newVal;//更新视图
}); //渲染文本节点时,实例化一个订阅者,并把Avue的实例化this传递过去,以及匹配到的{{message}},并执行回调,更新视图
}
}else if(node.nodeType===1){ //标签节点,比如<input type="text" a-model='name'>
let attrs=node.attributes; //获取标签里面的所有的属性
// console.log(attrs) ;//NamedNodeMap {0: type, 1: a-model, type: type, a-model: a-model, length: 2},attrs不是数组,所有需要转换为数组
Array.from(attrs).forEach(attr=>{
console.log(attr); //type='text' ,a-model='name', id='btn'
let attrName=attr.name; //返回type,a-mode,id
let attrValue=attr.value; //返回'text','name','btn'
if(attrName.indexOf('a-')==0){ //匹配'a-'
attrName=attrName.substr(2); //a-model去掉a- 即剩下的model
if(attrName==='model'){
node.value=this.$data[attrValue]; //即将name的值赋值给input标签
}
node.addEventListener('input',(e)=>{ //input标签监听input事件
this.$data[attrValue]= e.target.value;
})
new Watcher(this,attrValue,(newVal)=>{
node.value=newVal;//更新视图
}); //渲染标签节点时,实例化一个订阅者,并把Avue的实例化this传递过去,以及匹配到的a-model的值,并执行回调,更新视图
}
})
}
if(node.childNodes.length>0){//递归,处理很多层标签嵌套里面显示{{message}}
this.innerHandle(node)
}
})
}
}
class Dep{ //发布者
constructor(){
this.subArr=[]
}
addSub(sub){ //sub是一个定阅者,这里是将订阅者都Push进一个存储的数组中记录下来
this.subArr.push(sub)
}
notify(newVal){ //发布者向所有订阅者发布通知
this.subArr.forEach(sub=>{
sub.update(newVal);
})
}
}
class Watcher{
constructor(vm,key,callback){ //vm相当于实例化new Avue,key是data里面的key,比如message,name
this.callback=callback;//触发视图渲染的回调
Dep.target=this; //每次实例化new Watcher时,都将Dep.target指向触发实例化对象时的节点。
vm.$data[key]; // 相当于this.$data.message; 即获取一个message的值,即触发Object.defineProperty(this.$data,'message',{get(){
// if(Dep.target){
// dep.addSub(Dep.target)
// }
//}})里面 dep.addSub(Dep.target),即告诉发布者 我要订阅了
Dep.target=null; //发布者记录了该订阅者之后,即this.subArr.push(sub)之后,就清空Dep.target
}
update(newVal){
// console.log('我得到发布者要求更新的通知了,在这里执行更新');
this.callback(newVal); //回调,更新视图
}
}
注意:简单的实现了mvvm,但是没有实现data里面的数据是对象类型的渲染和双向绑定。即没有实现data:{form:{name:"Ace"}}这种data.form.name劫持和双向绑定。