mvvm的概念、原理及實現

  代碼實現來源於珠峯公開課mvvm原理的講解。此文在此記錄一下,經過手寫幾遍代碼加深一下本身對mvvm理解。javascript

一、MVVM的概念

  model-view-viewModel,經過數據劫持+發佈訂閱模式來實現。html

  mvvm是一種設計思想。Model表明數據模型,能夠在model中定義數據修改和操做的業務邏輯;view表示ui組件,負責將數據模型轉換爲ui展示出來,它作的是數據綁定的聲明、 指令的聲明、 事件綁定的聲明。;而viewModel是一個同步view和model的對象。在mvvm框架中,view和model之間沒有直接的關係,它們是經過viewModel來進行交互的。mvvm不須要手動操做dom,只須要關注業務邏輯就能夠了。   mvvm和mvc的區別在於:mvvm是數據驅動的,而MVC是dom驅動的。mvvm的優勢在於不用操做大量的dom,不須要關注model和view之間的關係,而MVC須要在model發生改變時,須要手動的去更新view。大量操做dom使頁面渲染性能下降,使加載速度變慢,影響用戶體驗。vue

二、mvvm的優勢

  • 一、低耦合性 view和model之間沒有直接的關係,經過viewModel來完成數據雙向綁定。
  • 二、可複用性 組件是能夠複用的。能夠把一些數據邏輯放到一個viewModel中,讓不少view來重用。
  • 三、獨立開發 開發人員專一於viewModel,設計人員專一於view。
  • 四、可測試性 ViewModel的存在能夠幫助開發者更好地編寫測試代碼。

三、mvvm的缺點

  • 一、bug很難被調試,由於數據雙向綁定,因此問題可能在view中,也可能在model中,要定位原始bug的位置比較難,同時view裏面的代碼無法調試,也添加了bug定位的難度。
  • 二、一個大的模塊中的model可能會很大,長期保存在內存中會影響性能。
  • 三、對於大型的圖形應用程序,視圖狀態越多,viewModel的構建和維護的成本都會比較高。

四、mvvm的雙向綁定原理

  mvvm的核心是數據劫持、數據代理、數據編譯和"發佈訂閱模式"。java

一、數據劫持——就是給對象屬性添加get,set鉤子函數。node

  • 一、觀察對象,給對象增長Object.defineProperty
  • 二、vue的特色就是新增不存在的屬性不會給該屬性添加get、set鉤子函數。
  • 三、深度響應。循環遞歸遍歷data的屬性,給屬性添加get,set鉤子函數。
  • 四、每次賦予一個新對象時(即調用set鉤子函數時),會給這個新對象進行數據劫持(defineProperty)。
//經過set、get鉤子函數進行數據劫持
function defineReactive(data){
    Object.keys(data).forEach(key=>{
        const dep=new Dep();
        let val=data[key];
        this.observe(val);//深層次的監聽
        Object.defineProperty(data,key,{
            get(){
                //添加訂閱者watcher(爲每個數據屬性添加訂閱者,以便實時監聽數據屬性的變化——訂閱)
                Dep.target&&dep.addSub(Dep.target);
                //返回初始值
                return val;
            },set(newVal){
                if(val!==newVal){
                    val=newVal;
                    //通知訂閱者,數據變化了(發佈)
                    dep.notify();
                    return newVal;
                }
            }
        })
    })
}
複製代碼

二、數據代理數組

  將data,methods,compted上的數據掛載到vm實例上。讓咱們不用每次獲取數據時,都經過mvvm._data.a.b這種方式,而能夠直接經過mvvm.b.a來獲取。緩存

class MVVM{
    constructor(options){
        this.$options=options;
        this.$data=options.data;
        this.$el=options.el;
        this.$computed=options.computed;
        this.$methods=options.methods;
        //劫持數據,監聽數據的變化
        new Observer(this.$data);
        //將數據掛載到vm實例上
        this._proxy(this.$data);
        //將方法也掛載到vm上
        this._proxy(this.$methods);
        //將數據屬性掛載到vm實例上
        Object.keys(this.$computed).forEach(key=>{
            Object.defineProperty(this,key,{
                get(){
                    return this.$computed[key].call(this);//將vm傳入computed中
                }
            })
        })
        //編譯數據
        new Compile(this.$el,this)
    };
    //私有方法,用於數據劫持
    _proxy(data){
        Object.keys(data).forEach(key=>{
            Object.defineProperty(this,key,{
                get(){
                    return data[key]
                }
            })
        })
        
    }
}    
複製代碼

三、數據編譯mvc

  把{{}},v-model,v-html,v-on,裏面的對應的變量用data裏面的數據進行替換。app

class Compile{
    constructor(el,vm){
        this.el=this.isElementNode(el)?el:document.querySelector(el);
        this.vm=vm;
        let fragment=this.nodeToFragment(this.el);
        //編譯節點
        this.compile(fragment);
        //將編譯後的代碼添加到頁面
        this.el.appendChild(fragment);
    };
    //核心編譯方法
    compile(node){
        const childNodes=node.childNodes;
        [...childNodes].forEach(child=>{
            if(this.isElementNode(child)){
                this.compileElementNode(child);
                //若是是元素節點就還得遞歸編譯
                this.compile(child);
            }else{
                this.compileTextNode(child);
            }
        }) 

    };
    //編譯元素節點
    compileElementNode(node){
        const attrs=node.attributes;
        [...attrs].forEach(attr=>{
            //attr是一個對象
            let {name,value:expr}=attr;
            if(this.isDirective(name)){
                //只考慮到v-html和v-model的狀況
                let [,directive]=name.split("-");
                //考慮v-on:click的狀況
                let [directiveName,eventName]=directive.split(":");
                //調用不一樣的指令來進行編譯
                CompileUtil[directiveName](node,this.vm,expr,eventName);
            }
        })
    };
    //編譯文本節點
    compileTextNode(node){
        const textContent=node.textContent;
        if(/\{\{(.+?)\}\}/.test(textContent)){
            CompileUtil["text"](node,this.vm,textContent)
        }
    };
    //將元素節點轉化爲文檔碎片
    nodeToFragment(node){
         //將元素節點緩存起來,統一編譯完後再拿出來進行替換
         let fragment=document.createDocumentFragment();
         let firstChild;
         while(firstChild=node.firstChild){
             fragment.appendChild(firstChild);
         }
         return fragment;
    };
    //判斷是不是元素節點
    isElementNode(node){
        return node.nodeType===1;
    };
    //判斷是不是指令
    isDirective(attr){
        return attr.includes("v-");
    }
}
//存放編譯方法的對象
CompileUtil={
    //根據data中的屬性獲取值,觸發觀察者的get鉤子
    getVal(vm,expr){
        const data= expr.split(".").reduce((initData,curProp)=>{
            //會觸發觀察者的get鉤子
            return initData[curProp];
        },vm)
        return data;
    },
    //觸發觀察者的set鉤子
    setVal(vm,expr,value){
        expr.split(".").reduce((initData,curProp,index,arr)=>{
            if(index===arr.length-1){
                initData[curProp]=value;
                return;
            }
            return initData[curProp];
        },vm)
    },
    getContentValue(vm,expr){
        const data= expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            return this.getVal(vm,args[1]);
        });
        return data;
    },
    model(node,vm,expr){ 
        const value=this.getVal(vm,expr);
        const fn=this.updater["modelUpdater"];   
        fn(node,value);
        //監聽input的輸入事件,實現數據響應式
        node.addEventListener('input',e=>{
            const value=e.target.value;
            this.setVal(vm,expr,value);
        })
        //觀察數據(expr)的變化,並將watcher添加到訂閱者隊列中
        new Watcher(vm,expr,newVal=>{
            fn(node,newVal);
        });
    },
    text(node,vm,expr){
        const fn=this.updater["textUpdater"];
        //將{{person.name}}中的person.james替換成james
        const content=expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            //觀察數據的變化
            new Watcher(vm,args[1],()=>{
                // this.getContentValue(vm,expr)獲取textContent被編譯後的值
                fn(node,this.getContentValue(vm,expr))

            })
            return this.getVal(vm,args[1]);
        })
        fn(node,content);
    },
    html(node,vm,expr){
        const value=this.getVal(vm,expr);
        const fn=this.updater["htmlUpdater"];
        fn(node,value);
        new Watcher(vm,expr,newVal=>{
            //數據改變後,再次替換數據
            fn(node,newVal);
        })
    },
    on(node,vm,expr,eventName){
        node.addEventListener(eventName,e=>{
            //調用call將vm實例(this)傳到方法中去
            vm[expr].call(vm,e);
        })
    },
    updater:{
        modelUpdater(node,value){
            node.value=value
        },
        htmlUpdater(node,value){
            node.innerHTML=value;
        },
        textUpdater(node,value){
            
            node.textContent=value;
        }
    }
}
複製代碼

四、發佈訂閱框架

  發佈訂閱主要靠的是數組關係,訂閱就是放入函數(就是將訂閱者添加到訂閱隊列中),發佈就是讓數組裏的函數執行(在數據發生改變的時候,通知訂閱者執行相應的操做)。消息的發佈和訂閱是在觀察者的數據綁定中進行數據的——在get鉤子函數被調用時進行數據的訂閱(在數據編譯時經過 new Watcher()來對數據進行訂閱),在set鉤子函數被調用時進行數據的發佈

//消息管理者(發佈者),在數據發生變化時,通知訂閱者執行相應的操做
class Dep{
    constructor(){
        this.subs=[];
    };
    //訂閱
    addSub(watcher){
        this.subs.push(watcher);
    };
    //發佈
    notify(){
        this.subs.forEach(watcher=>watcher.update());
    }
}
//訂閱者,主要是觀察數據的變化
class Watcher{
    constructor(vm,expr,cb){
        this.vm=vm;
        this.expr=expr;
        this.cb=cb;
        this.oldValue=this.get();
    };
    get(){
        Dep.target=this;
        const value=CompileUtil.getVal(this.vm,this.expr);
        Dep.target=null;
        return value;
    };
    update(){
        const newVal=CompileUtil.getVal(this.vm,this.expr);
        if(this.oldValue!==newVal){
            this.cb(newVal);
        }
    }
}
//觀察者
class Observer{
    constructor(data){
        this.observe(data);
    };
    //使數據可響應
    observe(data){
        if(data&&typeof data==="object"){
            this.defineReactive(data)
        }
    };
    defineReactive(data){
        Object.keys(data).forEach(key=>{
            const dep=new Dep();
            let val=data[key];
            this.observe(val);//深層次的監聽
            Object.defineProperty(data,key,{
                get(){
                    //添加訂閱者watcher(爲每個數據屬性添加訂閱者,以便實時監聽數據屬性的變化——訂閱)
                    Dep.target&&dep.addSub(Dep.target);
                    //返回初始值
                    return val;
                },set(newVal){
                    if(val!==newVal){
                        val=newVal;
                        //通知訂閱者,數據變化了(發佈)
                        dep.notify();
                        return newVal;
                    }
                }
            })
        })
    }
}
複製代碼
相關文章
相關標籤/搜索