Vue之MVVM原理詳解

什麼是MVVM?

MVVMModel-View-ViewModel的縮寫,它是一種基於前端開發的架構模式,ViewModel之間並無直接的聯繫,而是經過ViewModel進行交互,其核心是ViewModel經過雙向數據綁定ViewModel鏈接起來了,這使得View數據的變化會同步到Model中,而Model數據的變化也會當即反應到Viewjavascript


MVVM具體實現原理?

在Vue中使用數據劫持,採用Object.definePropertygettersetter,並結合觀察者模式來實現數據綁定。當把一個js對象傳給Vue實例來做爲它的data屬性時,Vue會遍歷它的屬性,用Object.defineProperty將它們賦予set和get,在內部它們讓Vue追蹤依賴,當屬性被訪問和修改時通知變化。 總體以下圖: html

具體分析如圖: 前端

Observer:數據監聽器,可以對數據對象的全部屬性進行監聽,若有變更可拿到最新值並通知訂閱者,內部採用Object.definePropertygetterSetter來實現的。vue

Compile:模板編譯,它的做用對每一個元素節點的指令和文本節點進行掃描和解析,根據指令模板替換數據,以及綁定相應的更新函數。java

Watcher:訂閱者,做爲鏈接ObserverCompile的橋樑,可以訂閱並收到每一個屬性變更的通知,執行指令綁定的相應回調函數。node

Dep:消息訂閱器,內部定義了一個數組,用來收集訂閱者(Watcher),數據變更觸發notify函數,再調用訂閱者的update方法。數組

分析上圖:當執行new Vue()時,Vue就進入了初始化階段,一方面Vue會遍歷data選項中的屬性,並用Object.defineProperty將它們轉換爲getter/setter,實現數據變化監聽功能;另外一方面,Vue的模板編譯Compile對元素節點的指令和文本節點進行掃描和解析,初始化視圖,Object.definePropertyget鉤子中addSub訂閱Watcher並添加到消息訂閱器(Dep)中,初始化完成。 當數據發生變化時,Observer中的setter方法被觸發,setter會當即調用Dep.notify()Dep開始遍歷全部的訂閱者,並調用訂閱者的update方法,訂閱者收到通知後對視圖進行相應的更新。架構

具體代碼實現?

Vue類實現

在入口Vue類中調用Observer進行數據劫持,將數據變成響應式數據;調用Compile模板編譯,找到須要替換數據的元素,進行編譯及初始化;最後進行數據代理,實現vm.school而不用使用vm.$data.school調用數據app

具體代碼以下:dom

class Vue{
    constructor(options){
        this.$el=options.el;
        this.$data=options.data;
        // 若是$el存在,那麼能夠找到上面的HTML模塊
        if(this.$el){
            // 把數據變成響應式 當 new Observer後,school就變成了響應式數據
            new Observer(this.$data)
            // 如今也須要讓vm代理this.$data
            this.proxyVm(this.$data)
            // console.log(this.$data)
            // 須要找到模塊中須要替換數據的元素,編譯模板
            new Compiler(this.$el,this)           
        }
    }
    // 讓vm代理data
    proxyVm(data){
        for(let key in data){ //data: {school:{name:beida,age:100}}
        // console.log(this) this vm實例
        Object.defineProperty(this,key,{
            get(){
                return data[key]
            }
        })

        }
    }
}
複製代碼

Compile--模板編譯類實現

主要分三步:

  1. 把真實的dom利用文檔碎片(fragment)移入到內存中,減少內存消耗,操做dom速度加快; 補充:將el中的內容移入到文檔碎片fragment中是一個進出棧的過程,el的子元素被移到fragment,出棧後,el的下一個子元素就會變成firstChild

  2. 編譯--遍歷元素節點和文本節點v-model...,{{}},而後執行相應的操做。 具體操做:

    1. 獲取元素的節點,提取其中的的指令或者模板**{{}}**
    2. 分類編譯指令的方法compileElement和編譯文本**{{}}**方法
  3. 把編譯好的fragment放回到原頁面中

具體代碼實現:

class Compiler{
    constructor(el,vm){
        this.el=this.isElementNode(el)?el:document.querySelector(el);
        this.vm=vm;
        // console.log(this.el)
        let fragment=this.node2fragment(this.el);
        // console.log(fragment);
        // 替換操做 (編譯模板) 用數據來編譯
        this.compile(fragment);

        // 把替換完的數據從新給網頁
        this.el.appendChild(fragment)



    }
    // 判斷一個屬性是不是一個指令
    isDirective(attrName){
        return attrName.startsWith("v-");   //返回的是boolean值
    }

    // 編譯元素節點
    compileElement(node){
        let attributes=node.attributes;   //獲得某個元素的屬性節點 是個僞數組
        // console.log(attributes)
        [...attributes].forEach(attr=>{
            let {name,value:expr}=attr;   //解構賦值
            // console.log(expr)
            if(this.isDirective(name)){
                // console.log(name+"是一個指令"); //v-model
                let [,directive]=name.split('-');
                // console.log(directive) //model,將v-去掉
                ComplierUtil[directive](node,expr,this.vm);
            }
        })

    }
    // 編譯文本節點
    compileText(node){
        let content=node.textContent;
        let reg=/\{\{(.+?)\}\}/;
        //reg.test(content) 若是content知足咱們寫的正則,返回true,不然false
        if(reg.test(content)){
            ComplierUtil["text"](node,content,this.vm);
        }

    }
    // 編譯
    compile(node){
        // childNodes並不包含li獲得的僅僅是子節點
        // console.log(node.childNodes) [text, input, text, div, text, div, text, ul, text]
        let childNodes=node.childNodes; 
        // console.log(Array.isArray(childNodes)) //獲得的childNodes是一個僞數組
        [...childNodes].forEach(child=>{  //[...childNodes]將僞數組childNodes轉變爲真正數組
            if(this.isElementNode(child)){
                // console.log(child+"是一個元素節點")
                this.compileElement(child);
                // 可能一個元素節點中嵌套其餘的元素節點,還可能嵌套文本節點
                // 若是child內部還有其餘節點,須要利用遞歸從新編譯
                this.compile(child);
            }else{
                // console.log(child+"獲得的是文本節點")
                this.compileText(child);
            }
        })


    }
    // 判斷一個節點是不是元素節點
    isElementNode(node){
        return node.nodeType===1;
    }
    // 將網頁的HTML移到文檔碎片中
    node2fragment(node){
        // 建立一個文檔碎片
        let fragment=document.createDocumentFragment();
        let firstChild;
        while(firstChild=node.firstChild){
            fragment.appendChild(firstChild);
        }
        return fragment;
    }
}



// 寫一個對象{},包含了不一樣的指令對應不一樣的處理方法
ComplierUtil={
    getVal(vm,expr){
        // console.log(expr.split(".")) // ["school","name"]
        // 第一次data是vm.$data即 school:{name:xx,age:xx},current 是school
        // 第二次data是school,current是name 即return data[current]==> school[current]
        return expr.split(".").reduce((data,current)=>{
            return data[current];
        },vm.$data);
        
    },
    setVal(vm,expr,value){
        // console.log(expr.split(".")) // ["school","name"]
        // 第一次data是vm.$data即 school:{name:xx,age:xx},current 是school,index是0,arr是["school","name"]
        // 第二次data是undefined(沒有處理累加,默認是undefined),current是name,index是1,arr是["school","name"]
        expr.split(".").reduce((data,current,index,arr)=>{
            // console.log(data)
            if(index==arr.length-1){
                // console.log(current)
                // console.log(data)
                return data[current]=value;
                // console.log(data[current])
                // console.log(111)
            }
            return data[current];
            
        },vm.$data)
    },
    model(node,expr,vm){  //node是帶指令的元素節點,expr是表達式,vm是vue對象
        let value=this.getVal(vm,expr)
        let fn=this.updater["modelUpdater"]
        // 給輸入框添加一個觀察者,若是後面數據發生改變了,就通知觀察者
        new Watcher(vm,expr,(newVal)=>{
            fn(node,newVal);
        })
        // 給input添加一個input事件,
        node.addEventListener("input",(e)=>{
            let value=e.target.value;
            this.setVal(vm,expr,value);
        })
        fn(node,value)
        
    },
    html(){

    },
    // 獲得新的內容
    getContentValue(vm,expr){
        return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            return this.getVal(vm,args[1])
        });
    },
    text(node,expr,vm){
        // console.log(node) //"{{school.name}}"
        // console.log(expr) //{{school.name}} {{school.age}}
        // console.log(vm)
        let content=expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            // console.log(vm)
            // console.log(args)
            new Watcher(vm,args[1],()=>{
                fn(node,this.getContentValue(vm,expr));
            })
            return this.getVal(vm,args[1])  //baida 100
        })
        let fn=this.updater["textUpdater"];

        fn(node,content)

    },
    // 更新數據
    updater:{
        modelUpdater(node,value){
            node.value=value;
        },
        htmlUpdater(){

        },
        // 處理文本節點
        textUpdater(node,value){
            // textContent獲得文本節點中內容
            node.textContent=value
        }

    }
}

複製代碼

Observer--數據劫持-實現雙向數據綁定

// 實現數據的響應式--->數據劫持,當獲取修改數據時,須要感應到(set和get)
class Observer{
    constructor(data){
        this.observer(data)
    }
    // 把上面的數據變成響應式數據,把一個對象數據作出響應式
    observer(data){
        if(data&& typeof data=='object'){
            // console.log(data) //{school: {name: "beida", age: 100}}
            // for in 循環一個js對象
            for(let key in data){
                // console.log(key) //school
                // console.log(data[key]) //{name: "beida", age: 100}
                this.defindReactive(data,key,data[key])


            }
        }
    }
    defindReactive(obj,key,value){
        this.observer(value)  //若是一個數據是一個對象,也須要將其變成響應式
        // Object.defineProperty(obj,prop,descriptor)函數會直接在obj上定義一個新屬性或修改一個新屬性
        // obj要在其上定義屬性或修改的對象,prop要定義或修改的屬性名稱,descriptor 將被定義或修改的屬性描述符
        // 這是要修改obj對象的school屬性
        let dep=new Dep();   //不一樣的watcher放到不一樣的dep中
        Object.defineProperty(obj,key,{
            // 修改以下 當獲取school時,會調用get
            get(){
                Dep.target&&dep.subs.push(Dep.target)
                // console.log("get...")
                return value
            },
            // 當設置school時,會調用set
            set:(newVal)=>{
                if(newVal!=value){
                    // console.log("set...")
                    this.observer(newVal)
                    value=newVal;
                    // 值改變時,通知觀察者
                    dep.notify();

                }
            }

        })

    }
}
複製代碼

Watcher--訂閱者

// 觀察者
class Watcher{
    constructor(vm,expr,cb){
        this.vm=vm;
        this.expr=expr;
        this.cb=cb;
        // 剛開始須要一個老的狀態
        this.oldValue=this.get();

    }
    get(){
        Dep.target=this;
        let value=ComplierUtil.getVal(this.vm,this.expr);
        Dep.target=null;
        return value;
    }
    // 當狀態改變後,會調用觀察者的update
    update(){
        let newVal=ComplierUtil.getVal(this.vm,this.expr);
        if(newVal!=this.oldValue){
            this.cb(newVal)
        }
    }
}

複製代碼

Dep--消息訂閱器

// 存儲觀察者的類Dep
class Dep{
    constructor(){
        this.subs=[];  //在subs中存放因此的watcher
    }
    // 添加watcher即訂閱
    addSub(watcher){
        this.subs.push(watcher)
    }
    // 通知 發佈 通知subs容器中的全部觀察者
    notify(){
        this.subs.forEach(watcher=>watcher.update())
    }
}
複製代碼

完整代碼以下:

// 存儲觀察者的類Dep
class Dep{
    constructor(){
        this.subs=[];  //在subs中存放因此的watcher
    }
    // 添加watcher即訂閱
    addSub(watcher){
        this.subs.push(watcher)
    }
    // 通知 發佈 通知subs容器中的全部觀察者
    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;
        let value=ComplierUtil.getVal(this.vm,this.expr);
        Dep.target=null;
        return value;
    }
    // 當狀態改變後,會調用觀察者的update
    update(){
        let newVal=ComplierUtil.getVal(this.vm,this.expr);
        if(newVal!=this.oldValue){
            this.cb(newVal)
        }
    }
}



// 實現數據的響應式--->數據劫持,當獲取修改數據時,須要感應到(set和get)
class Observer{
    constructor(data){
        this.observer(data)
    }
    // 把上面的數據變成響應式數據,把一個對象數據作出響應式
    observer(data){
        if(data&& typeof data=='object'){
            // console.log(data) //{school: {name: "beida", age: 100}}
            // for in 循環一個js對象
            for(let key in data){
                // console.log(key) //school
                // console.log(data[key]) //{name: "beida", age: 100}
                this.defindReactive(data,key,data[key])


            }
        }
    }
    defindReactive(obj,key,value){
        this.observer(value)  //若是一個數據是一個對象,也須要將其變成響應式
        // Object.defineProperty(obj,prop,descriptor)函數會直接在obj上定義一個新屬性或修改一個新屬性
        // obj要在其上定義屬性或修改的對象,prop要定義或修改的屬性名稱,descriptor 將被定義或修改的屬性描述符
        // 這是要修改obj對象的school屬性
        let dep=new Dep();   //不一樣的watcher放到不一樣的dep中
        Object.defineProperty(obj,key,{
            // 修改以下 當獲取school時,會調用get
            get(){
                Dep.target&&dep.subs.push(Dep.target)
                // console.log("get...")
                return value
            },
            // 當設置school時,會調用set
            set:(newVal)=>{
                if(newVal!=value){
                    // console.log("set...")
                    this.observer(newVal)
                    value=newVal;
                    // 值改變時,通知觀察者
                    dep.notify();

                }
            }

        })

    }
}



class Compiler{
    constructor(el,vm){
        this.el=this.isElementNode(el)?el:document.querySelector(el);
        this.vm=vm;
        // console.log(this.el)
        let fragment=this.node2fragment(this.el);
        // console.log(fragment);
        // 替換操做 (編譯模板) 用數據來編譯
        this.compile(fragment);

        // 把替換完的數據從新給網頁
        this.el.appendChild(fragment)



    }
    // 判斷一個屬性是不是一個指令
    isDirective(attrName){
        return attrName.startsWith("v-");   //返回的是boolean值
    }

    // 編譯元素節點
    compileElement(node){
        let attributes=node.attributes;   //獲得某個元素的屬性節點 是個僞數組
        // console.log(attributes)
        [...attributes].forEach(attr=>{
            let {name,value:expr}=attr;   //解構賦值
            // console.log(expr)
            if(this.isDirective(name)){
                // console.log(name+"是一個指令"); //v-model
                let [,directive]=name.split('-');
                // console.log(directive) //model,將v-去掉
                ComplierUtil[directive](node,expr,this.vm);
            }
        })

    }
    // 編譯文本節點
    compileText(node){
        let content=node.textContent;
        let reg=/\{\{(.+?)\}\}/;
        //reg.test(content) 若是content知足咱們寫的正則,返回true,不然false
        if(reg.test(content)){
            ComplierUtil["text"](node,content,this.vm);
        }

    }
    // 編譯
    compile(node){
        // childNodes並不包含li獲得的僅僅是子節點
        // console.log(node.childNodes) [text, input, text, div, text, div, text, ul, text]
        let childNodes=node.childNodes; 
        // console.log(Array.isArray(childNodes)) //獲得的childNodes是一個僞數組
        [...childNodes].forEach(child=>{  //[...childNodes]將僞數組childNodes轉變爲真正數組
            if(this.isElementNode(child)){
                // console.log(child+"是一個元素節點")
                this.compileElement(child);
                // 可能一個元素節點中嵌套其餘的元素節點,還可能嵌套文本節點
                // 若是child內部還有其餘節點,須要利用遞歸從新編譯
                this.compile(child);
            }else{
                // console.log(child+"獲得的是文本節點")
                this.compileText(child);
            }
        })


    }
    // 判斷一個節點是不是元素節點
    isElementNode(node){
        return node.nodeType===1;
    }
    // 將網頁的HTML移到文檔碎片中
    node2fragment(node){
        // 建立一個文檔碎片
        let fragment=document.createDocumentFragment();
        let firstChild;
        while(firstChild=node.firstChild){
            fragment.appendChild(firstChild);
        }
        return fragment;
    }
}



// 寫一個對象{},包含了不一樣的指令對應不一樣的處理方法
ComplierUtil={
    getVal(vm,expr){
        // console.log(expr.split(".")) // ["school","name"]
        // 第一次data是vm.$data即 school:{name:xx,age:xx},current 是school
        // 第二次data是school,current是name 即return data[current]==> school[current]
        return expr.split(".").reduce((data,current)=>{
            return data[current];
        },vm.$data);
        
    },
    setVal(vm,expr,value){
        // console.log(expr.split(".")) // ["school","name"]
        // 第一次data是vm.$data即 school:{name:xx,age:xx},current 是school,index是0,arr是["school","name"]
        // 第二次data是undefined(沒有處理累加,默認是undefined),current是name,index是1,arr是["school","name"]
        expr.split(".").reduce((data,current,index,arr)=>{
            // console.log(data)
            if(index==arr.length-1){
                // console.log(current)
                // console.log(data)
                return data[current]=value;
                // console.log(data[current])
                // console.log(111)
            }
            return data[current];
            
        },vm.$data)
    },
    model(node,expr,vm){  //node是帶指令的元素節點,expr是表達式,vm是vue對象
        let value=this.getVal(vm,expr)
        let fn=this.updater["modelUpdater"]
        // 給輸入框添加一個觀察者,若是後面數據發生改變了,就通知觀察者
        new Watcher(vm,expr,(newVal)=>{
            fn(node,newVal);
        })
        // 給input添加一個input事件,
        node.addEventListener("input",(e)=>{
            let value=e.target.value;
            this.setVal(vm,expr,value);
        })
        fn(node,value)
        
    },
    html(){

    },
    // 獲得新的內容
    getContentValue(vm,expr){
        return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            return this.getVal(vm,args[1])
        });
    },
    text(node,expr,vm){
        // console.log(node) //"{{school.name}}"
        // console.log(expr) //{{school.name}} {{school.age}}
        // console.log(vm)
        let content=expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            // console.log(vm)
            // console.log(args)
            new Watcher(vm,args[1],()=>{
                fn(node,this.getContentValue(vm,expr));
            })
            return this.getVal(vm,args[1])  //baida 100
        })
        let fn=this.updater["textUpdater"];

        fn(node,content)

    },
    // 更新數據
    updater:{
        modelUpdater(node,value){
            node.value=value;
        },
        htmlUpdater(){

        },
        // 處理文本節點
        textUpdater(node,value){
            // textContent獲得文本節點中內容
            node.textContent=value
        }

    }
}






class Vue{
    constructor(options){
        this.$el=options.el;
        this.$data=options.data;
        // 若是$el存在,那麼能夠找到上面的HTML模塊
        if(this.$el){
            // 把數據變成響應式 當 new Observer後,school就變成了響應式數據
            new Observer(this.$data)
            // 如今也須要讓vm代理this.$data
            this.proxyVm(this.$data)
            // console.log(this.$data)
            // 須要找到模塊中須要替換數據的元素,編譯模板
            new Compiler(this.$el,this)           
        }
    }
    // 讓vm代理data
    proxyVm(data){
        for(let key in data){ //data: {school:{name:beida,age:100}}
        // console.log(this) this vm實例
        Object.defineProperty(this,key,{
            get(){
                return data[key]
            }
        })

        }
    }
}
複製代碼
相關文章
相關標籤/搜索