数据劫持实现mvvm(简单模拟vue实现数据驱动视图/以及双向绑定)

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劫持和双向绑定。

发布了200 篇原创文章 · 获赞 37 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/qq_42231156/article/details/103985643