vue2 MVVM 响应式原理

目录

双向数据绑定

数据劫持

Compile:

 Observer :

Dep:

Watcher:

 简单代码实现

 index.html 

MVue.js

 Observer.js

总结:面试阐述


双向数据绑定

数据劫持

vue.js采用的是数据劫持结合发布者-订阅模式,通过Object.defineProperty()来劫持各个属性的Setter和Getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

图解:

Compile:

 解析指令,并初始化视图,同时订阅数据变化,通过Watcher绑定更新函数

 Observer :

 劫持监听所有属性,向Dep通知变化

Dep:

 向Watcher通知变化

Watcher:

 向Dep添加订阅者,并触发Updater更新视图

   简单实现

  1. 创建一个模板(index.html),引入MVue.js和Observer.js。仿照出类似于vue.js的指令,包含(v-text、v-html、v-model、v-bind、v-on:click等,并使用{{变量名}}的方式)
  2. 创建Class MVue初始化构造函数el,data。分别实现Compile 和 Observer
  3. 创建Class Compile,首先判断el是否为根节点,随后将根节点通过循环appendChild全部追加到文档碎片对象当中(document.createDocumentFragment()).,进行模板编译。将文档碎片的内容追加回el当中。
  4. 在编译(compile(fragment))的过程中,通过解构的方式遍历区分出出元素节点(node.nodeType === 1),和文本节点,采用不同的方式编译
  5. 元素节点拆分出所有属性(node.attributes),然后通过解构遍历出属性名和属性值。随后通难过属性名startsWith()找出所有'v-'开头的指令,@开头的事件,以及:开头的bind绑定。解构过滤出所有属性名
  6. 将过滤出来的{{变量名}}表达式传入compileUtils['属性名'](例如:compileUtils['text'])方法用来解析指令表达式和值。通过expr.split('.').reduce((data,curr)=>{return data[curr]},vm.$data)来获取data中的变量值(如果是v-text,则需要expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{},去除花括号,取出args[1]在进行reduce累加)
  7. 将累加查找出的变量值更新到视图中,其中每种类型的节点属性都有不同的方式(textNode.textContent = value,htmlNode.innerHTML = value,modelNode.value =value,bindNode[arrtName]=value),解析指令完成
  8. 文本节点({{...}})则可以直接使用创建的CompileUtils来进行text解析
  9. 创建Observer.js ,创建Observer类。初始化调用方法,判断数据类型是否为对象,遍历出data中所有的key值,(object.keys(data).forEach(key=>{})),用获取到的Key值,获取value(data[key]),将value再次判断数据类型,递归深层对象,直到拿到对象所有的属性值
  10. 通过获取到的data key,value 使用Object.defineProperty(obj,key,{get(){},set:()=>{}})劫持监听data中所有的属性值得变化。
  11. 创建Class Dep订阅器,也称观察者收集器。初始化一个数组arr进行收集watcher.添加方法addWatcjer(),添加方法notify用来通知观察者更新(this.arr.forEach(w=>w.update())).
  12. 当订阅数据发生变化时(defineProperty.get()获取数据更新时)往Dep中添加watcher。当数据发生改变时(defineProperty.set()),通知dep将变化通知给watcher
  13. 创建Class Watcher,用来观察调用更新视图初始化存储旧的数据,等到dep.notify()通知随后进行更新回调,通过对比新老数据,决定是否更新。
  14. 观察者应当绑定在所有数据要发生变化的地方,来绑定劫持每个属性的变化。通过回调对比新老数据。将新数据更新在视图当中。
  15. 表单双向数据绑定:添加input事件监听,将input值set到data[currVal]当中

 简单代码实现

 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">
        <h1>{{msg}}</h1>
        <h1>{{person.name}}--{{person.age}}</h1>
        <h1>{{person.fav}}</h1>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
        <div v-text="msg"></div>
        <div v-html="htmlStr">111</div>
        <input type="text" v-model="msg">
        <a :href="link">跳转</a>
        <button v-on:click="handleClick">on按钮</button>
        <button @click="handleClick">@按钮</button>
    </div>
    
    <script src="./Observer.js"></script>
    <script src="./MVue.js"></script>
    <script >

        let vm = new MVue({
            el:'#app',
            data:{
                person:{
                    name:"kiki",
                    age:23,
                    fav:'姑娘'
                },
                msg:'学习MVVM实现原理',
                link:'http://www.baidu.com',
                htmlStr:'<h1>htmlStr</h1>'
                
            },
            methods:{
                handleClick(){
                    console.log(this);
                    // this.$data.person.name = '更改了person.name';
                    this.person.name = 'proxy取值';//proxy取值
                }
            }
        })
    </script>

</body>
</html>

MVue.js

const compileUtils = {
    getVal(expr,vm){
        return expr.split('.').reduce((data,currentVal)=>{
            //console.log("currentVal:"+currentVal);//msg
            return data[currentVal];
        },vm.$data)
    },
    setVal(expr,vm,inputVal){
        return expr.split('.').reduce((data,currentVal)=>{
            data[currentVal] = inputVal;
        },vm.$data)
    },
    getContentVal(expr,vm){
        return value = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            return this.getVal(args[1],vm);
        })
    },

    text(node,expr,vm){//expr:msg 学习MVVM原理
        let value;
        if(expr.indexOf('{{') !== -1){
            value = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
               //console.log(args);// ['{{msg}}', 'msg', 0, '{{msg}}'] ['{{person.name}}', 'person.name', 0, '{{person.name}}--{{person.age}}']
               //绑定观察者,将来数据反生变化,触发这里的回调进行更新 
               new Watcher(vm,args[1],(newVal)=>{
                    this.updater.textUpdater(node,this.getContentVal(expr,vm));
                })
                return this.getVal(args[1],vm);
            });
        }else{
            value =this.getVal(expr,vm);
        }
        this.updater.textUpdater(node,value);
    },
    html(node,expr,vm){
        let value =this.getVal(expr,vm);
        new Watcher(vm,expr,(newVal)=>{
            this.updater.htmlUpdater(node,newVal);
        });
        this.updater.htmlUpdater(node,value);
    },
    model(node,expr,vm){
        //绑定更新函数,数据=>视图
        const value =this.getVal(expr,vm);
        new Watcher(vm,expr,(newVal)=>{
            this.updater.modelUpdater(node,newVal);
        });
        //视图=>数据=>视图
        node.addEventListener('input',(e)=>{
            //设置值
            this.setVal(expr,vm,e.target.value);
        })
        this.updater.modelUpdater(node,value);
    },
    on(node,expr,vm,eventName){
        let fn = vm.$options.methods && vm.$options.methods[expr];
        node.addEventListener(eventName,fn.bind(vm),false);//fn.bind(vm)绑定实例事件
    },
    bind(node,expr,vm,attrName){
        //自己实现
        const value =this.getVal(expr,vm);
        this.updater.bindUpdater(node,value,attrName);
    },
    //更新函数
    updater:{
        textUpdater(node,value){
            // console.log(node,value);
            node.textContent = value;
        },
        htmlUpdater(node,value){
            
            node.innerHTML = value;
        },
        modelUpdater(node,value){
            
            node.value = value;
        },
        bindUpdater(node,value,attrName){
           
            node[attrName]= value;
        }
    }
}
//解析指令,初始化视图,订阅数据变化,绑定更新函数
class Compile{
    constructor(el,vm){
        this.el = this.isElementNode(el)? el : document.querySelector(el);
        this.vm = vm;
        //获取文档碎片对象,放入内存中减少页面的回流和重绘
        const fragment = this.node2Fragment(this.el);
        // console.log(fragment);
        //编译模板
        this.compile(fragment);

        //追加子元素到根元素
        this.el.appendChild(fragment);
    }
    //编译方法
    compile(fragment){
        const childNodes = fragment.childNodes;
        [...childNodes].forEach(child => {
            if(this.isElementNode(child)){
                // console.log('元素节点',child);
                this.compileElement(child);
            }else{
                // console.log('文本节点',child);
                this.compileText(child);
            }

            //递归遍历出所有子节点元素
            if(child.childNodes && child.childNodes.length){
                this.compile(child);
            }
        })
    }
    //编译节点
    compileElement(node){
        // console.log("eleNode:"+node);
        const attributes = node.attributes;
        [...attributes].forEach(attr =>{
            // console.log(attr);
            const {name,value} = attr;//解构属性和值
            if(this.isDirective(name)){//属于指令 v-text v-html v-model v-on:click...
                const [,dirctive] = name.split('-');//text html model on:click
                const [dirName,eventName] = dirctive.split(':');//事件名分割 //on click
                //更新数据,数据驱动视图
                compileUtils[dirName](node,value,this.vm,eventName);
                //删除有指令的标签上的属性
                node.removeAttribute('v-' + dirctive);

            }else if(this.isEventName(name)){//@事件,v-on简写
                let [,eventName] = name.split('@');
                compileUtils['on'](node,value,this.vm,eventName);
            }else if(this.isAttrsName(name)){//:事件,v-bind简写
                let [,eventName] = name.split(':');
                compileUtils['bind'](node,value,this.vm,eventName);
            }
        })
    }
    //判断是否是'v-'开头的指令
    isDirective(attrName){
        return attrName.startsWith('v-');
    }
    isEventName(attrName){
        return attrName.startsWith('@');
    }
    isAttrsName(attrName){
        return attrName.startsWith(':');
    }
    //编译文本
    compileText(node){
        // console.log("textNode"+node);
        const content = node.textContent;
        if(/\{\{(.+?)\}\}/.test(content)){
            // console.log(content);
            compileUtils['text'](node,content,this.vm)
        }

    }

    //将所有获取到的节点元素追加到文档碎片对象中
    node2Fragment(el){
        //创建文档碎片对象
        const f = document.createDocumentFragment();
        let firstChild;
        while(firstChild = el.firstChild){
            f.appendChild(firstChild);
        }
        return f;
    }
    //判断是否是节点元素
    isElementNode(node){
       return node.nodeType === 1;
    }
}

//入口
class MVue{
    constructor(options){
        this.$el= options.el;
        this.$data = options.data;
        this.$options = options;
        if(this.$el){
            //实现一个数据的观察者
            new Observer(this.$data);
            //实现指令解析器
            new Compile(this.$el,this);
            //proxy代理,将vm.$data.attr 简化为vm[attr]直接取值
            this.proxyData(this.$data);
        }
    }
    proxyData(data){
        for(const key in data){
            Object.defineProperty(this,key,{
               get(){
                return data[key];
               },
               set(newVal){
                data[key] = newVal;
               }
            })
        }
    }
}

 Observer.js

//观察者,添加到订阅器,通知updater更新视图
class Watcher{
    constructor(vm,expr,cb){
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        //先将旧值保存起来
        this.oldVal = this.getOldVal();
    }
    getOldVal(){
        Dep.target= this;
        const oldVal = compileUtils.getVal(this.expr,this.vm);
        Dep.target = null;
        return oldVal;
    }
    update(){
        const newVal = compileUtils.getVal(this.expr,this.vm);
        if(newVal !== this.oldVal){
            this.cb(newVal)
        }
    }
}

//订阅器,收集观察者Watcher,通知观察者变化
class Dep {
    constructor(){
        this.subs = [];
    }
    //收集观察者
    addSub(watcher){
        this.subs.push(watcher);
    }
    //通知观察者去更新
    notify(){
        console.log("通知了观察者,this.subs:",this.subs);
        this.subs.forEach(w=>w.update());//遍历所有观察者进行更新
    }
}

//劫持监听所有属性,通知Dep
class Observer{
    constructor(data) {
        this.observer(data)
        
    }
    observer(data){
        if(data && typeof data === 'object'){
        //    console.log( Object.keys(data));
        Object.keys(data).forEach(key=>{
            this.defineReactive(data,key,data[key]);
        })
            
        }
    }
    defineReactive(obj,key,value){
        //递归劫持深层次对象
        this.observer(value);
        const dep = new Dep();
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:false,
            get(){
                //初始化
                //订阅数据发生变化时,往Dep中添加观察者
                Dep.target &&  dep.addSub(Dep.target);
                return value;
            },
            set:(newVal)=>{
                this.observer(newVal);
                if(newVal !== value){
                    value = newVal;
                }
                //告诉Dep通知变化
                dep.notify();
            }
        })
    }
}

总结:面试阐述

vue 是采用数据劫持配合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter  和 getter,在数据变动时,发布消息给依赖收集器,通知观察者做出对应的回调函数,然后更新视图。

MVVM作为绑定的入口,整合Observer,Compile和Watcher三者,同构Observer来监听model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer,Compile之间的通讯桥梁,达到数据变化=>视图更新,视图交互变化=>数据model变更的双向数据绑定。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值