vue之mvvm原理解析

時光_靜好發表於2019-10-12

vue之mvvm原理解析

路過的朋友,可以點個贊,關注一下~~~

mvvm 面試論述


MVVM分為Model、View、ViewModel三者

  • Model:代表資料模型,資料和業務邏輯都在Model層中定義;
  • View:代表UI檢視,負責資料的展示;
  • ViewModel:負責監聽Model中資料的改變並且控制檢視的更新,處理使用者互動操作;

這種模式實現了ModelView的資料自動同步,也就是雙向繫結,mvvm雙向繫結,採用的是資料劫持結合釋出者-訂閱者模式的方式,通過Object.defineProperty() 來劫持各個屬性的 setter、getter,在資料變動時釋出訊息給訂閱者,觸發相應的監聽回撥。

大致的過程:

  1. 實現一個指令解析器Compile,對每個元素節點的之類進行解析,根據指令模板替換資料,以及搬到相應的更新函式

  2. 實現一個資料監控器Observer,將所有資料設定成響應式,並進行監聽,如有變動可以拿到最新值並通知訂閱者

  3. 實現一個訂閱者Watcher,作為連線Observer(資料劫持)Compile(模板) 的橋樑,在對應模板資料更新處,新增監聽資料的訂閱者,並將其新增到訂閱者容器Dep中,當屬性變動時,通過Dep釋出通知,執行指令繫結的相應回撥函式,從而更新檢視

  4. mvvm的入口函式,主要是整合調控以上的,模板編譯(compile)資料劫持(Observe)訂閱者(Watcher)

mvvm的編譯過程以及使用


  • 編譯的流程圖

編譯流程圖

  • 整體分析

整體分析

過程分析new MVVM() 後的編譯主要分為兩個部分

  1. 一部分是模板的編譯 Compile

    • 編譯元素和文字,將插值表示式進行替換
    • 編譯模板指令的標籤,例如:v-model
  2. 一部分是資料劫持 Observer

    • 將所有的資料響應式處理
    • 給模板的每個編譯處設定一個觀察者,並將觀察者存放在Dep中
    • Watcher 如果資料發生改變,在ObjectdefinePropertyset函式中呼叫Watcher的update方法
    • Dep 釋出訂閱,將所有需要通知變化的data新增到一個陣列中

具體步驟

1、需要 observe 的資料物件進行遞迴遍歷,包括子屬性物件的屬性,都加上 setter 和 getter,這樣的話,給這個物件的某個值賦值,就會觸發 setter,那麼就能監聽到了資料變化

2、 compile 解析模板指令,將模板中的變數替換成資料,然後初始化渲染頁面檢視,並將每個指令對應的節點繫結更新函式,新增監聽資料的訂閱者,一旦資料有變動,收到通知,更新檢視

3、 Watcher 訂閱者是 Observer 和 Compile 之間通訊的橋樑,主要做的事情是:

  • 在自身例項化時往屬性訂閱器(dep)裡面新增自己
  • 自身必須有一個 update() 方法
  • 待屬性變動 dep.notice() 通知時,能呼叫自身的 update() 方法,並觸發 Compile 中繫結的回撥,則功成身退。

4、MVVM 作為資料繫結的入口,整合 Observer、Compile 和 Watcher 三者,通過Observer來監聽自己的 model 資料變化,通過 Compile 來解析編譯模板指令,最終利用 Watcher 搭起 Observer 和 Compile 之間的通訊橋樑,達到資料變化 -> 檢視更新;檢視互動變化(input) -> 資料 model 變更的雙向繫結效果。

分解vue例項


vue的使用,

let vm = new Vue({
		el:"#app",
		data:{
			school:{
				name:"beida",
				age:100
			}
		},
	})
複製程式碼

首先使用new建立一個vue例項,傳遞一個物件引數,包含eldata

實現Complie編譯模板


index.html頁面的使用

enter description here

vue類-入口檔案


enter description here

在入口之處,先處理了模板的編譯(Compile),資料劫持(Observe)在後期進行使用。

編譯模板


編譯模板的主要的入口,分為,將節點轉成文件碎片,替換模板中的常量資料

enter description here

節點轉文件碎片

enter description here

將節點轉換成文件碎片,然後返回,

編譯模板

//編譯模板
class Compiler{


    //判斷一個節點是否是元素節點
    isElementNode(node){
        return node.nodeType ```= 1;
    }

    //判斷一個屬性是否是一個指令
    isDirective(attrName){
        return attrName.startsWith("v-");
    }

    //編譯模板
    compile(node){
        //node.childNodes 包含了元素節點與文字節點
        //得到的是一個偽陣列
        let childNodes = node.childNodes;
        [...childNodes].forEach(child=>{
            if(this.isElementNode(child)){  //元素節點
                //編譯元素節點
                this.compileElementNode(child)
                //遞迴編譯所有的節點
                this.compile(child)

            }else{  //文字節點
                //編譯文字節點
                this.compileText(child)
            }
        })
    }

    //編譯元素節點
    compileElementNode(node){
        //獲取元素的屬性節點(偽陣列)
        let attributes = node.attributes;
        [...attributes].forEach(attr=>{
            let {name,value: expr} = attr;

            //判斷是否是一個指令
            if(this.isDirective(name)){
                let [,directive] = name.split("-")
                
                //根據指令,呼叫對呀的指令方法
                CompilerUtil[directive](node,expr,this.vm);
            }
        })
        
    }

    //編譯文字節點
    compileText(node){
        //得到所有的文字節點
        let content = node.textContent;
        //使用正則得到所有文字里面的內容
        let reg = /\{\{(.+?)\}\}/;
        if(reg.test(content)){

            //{{}} 是v-text的語法糖,所有呼叫text指令
            CompilerUtil['text'](node,content,this.vm)
        }
        
    }


    //將節點轉成文件碎片
    node2fragme(node){
        //建立鍵一個文件碎片
        let fragment = document.createDocumentFragment();
        let firstChild ;
        while(firstChild = node.firstChild){
            fragment.appendChild(firstChild)
        }
        return fragment;
    }

    
}

//編譯指令處理物件--處理不同的指令
CompilerUtil = {

    //獲取到data中對應的資料 
    getVal(vm,expr){
        return  expr.split(".").reduce((data,current)=>{
              return data[current]
          },vm.$data)
    },

    //設定$data中的資料
    setVal(vm,expr,value){
        expr.split(".").reduce((data,current,index,arr)=>{
            if(index ``` arr.length -1){
                return data[current] = value;
            }
            return data[current]
        },vm.$data)
    },

    //處理 v-model 指令的資料
    model(node,expr,vm){
        
        
        //更新模板中在data中對應的資料
        let fn = this.updater['modelUpdater']

        //當input 框的資料相互繫結
        node.addEventListener("input",(e)=>{
            let value = e.target.value;
            //當輸入框資料改變時,同步更改$data中的資料
            this.setVal(vm,expr,value);
        })

        let value = this.getVal(vm,expr)
        //替換模板中的資料
        fn(node,value)
    },

    // 處理v-text指令的資料
    text(node,expr,vm){

        let fn = this.updater['textUpdater'];
        //獲取到要替換的內容
        let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            return this.getVal(vm,args[1])
        })
        fn(node,content)
    },

    //更新模板中的資料
    updater:{
        //將v-model繫結的資料進行替換
        modelUpdater(node,value){
            node.value = value;
        },

        //將v-text繫結的資料進行替換
        textUpdater(node,content){
            node.textContent = content;
        }
    }
}

複製程式碼

編譯模板,主要是對模板中的一些資料常量進行替換,對於一些指令進行相關的處理,特別是指令v-model的資料的繫結。

資料劫持


資料劫持,實在模板進行編譯之前進行,將data中的所有的資料都變成響應式資料,

Observer的呼叫 vue類

enter description here

資料劫持 observer類

//資料劫持,將資料變成響應式資料
class Observer{
    constructor(data){
        //將資料變成響應式資料
        this.observer(data)
    }

    //將資料變成響應式資料
    observer(data){
        //判斷資料是否是一個物件
        if(data && typeof data ``` 'object'){
            for(let key in data){
                //設定響應式
                this.defindReactive(data,key,data[key])
            }
        }
    }

    //設定響應式
    defindReactive(obj,key,value){
        //如果資料是一個物件,繼續遞迴設定
        this.observer(value);
        let dep = new Dep();    //不用的watcher存放到不同的dep中
        Object.defineProperty(obj,key,{

            //當獲取資料時會呼叫get
            get(){
                return value;
            },

            //當設定資料時會呼叫set
            set: (newValue)=>{
                if(newValue != value){
                    //將新資料設定成響應式
                    this.observer(newValue);
                    value = newValue;
                }
            }
        })
        
    }
}
複製程式碼

訂閱者的Watcher的實現


訂閱者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 = CompilerUtil.getVal(this.vm,this.expr)
        Dep.target = null;
        return value;
    }

    //當狀態發生改變的時候,觀察者更新當前的狀態
    update(){
        let newVal = CompilerUtil.getVal(this.vm,this.expr);
        if(this.oldValue !``` newVal){
            this.cb(newVal)
        }
    }
}
複製程式碼

存放訂閱者Dep

//儲存觀察者的類
class Dep {
    constructor(){
        this.subs = []; //存放所有的watcher
    }
    //新增watcher 訂閱
    addSub(watcher){
        this.subs.push(watcher)
    }

    //通知釋出
    notify(){
        this.subs.forEach(watcher=>watcher.update())
    }
}
複製程式碼

訂閱者,連線編譯模板與資料劫持

編譯模板處

//編譯指令處理物件--處理不同的指令
CompilerUtil = {

     // 此處省略若該程式碼 ......

    //處理 v-model 指令的資料
    model(node,expr,vm){
        
        //更新模板中在data中對應的資料
        let fn = this.updater['modelUpdater']

        
        //給輸入框新增一個觀察者,如果資料改變,通知data資料改變
        new Watcher(vm,expr,(newValue) =>{
            fn(node,newValue)
        })

        // 此處省略若該程式碼 ......
    },

    // 處理v-text指令的資料
    text(node,expr,vm){

        let fn = this.updater['textUpdater'];
        let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{

            //新增一個訂閱者
            new Watcher(vm,args[1],()=>{
                fn(node,this.getVal(vm,args[1]))
            })
            return this.getVal(vm,args[1])
        })
        fn(node,content)
    },

     // 此處省略若該程式碼 ......
}
複製程式碼

資料劫持處

//資料劫持,將資料變成響應式資料
class Observer{

    // 此處省略若該程式碼 ...

    //設定響應式
    defindReactive(obj,key,value){
        // 此處省略若該程式碼 ...

        Object.defineProperty(obj,key,{

            //當獲取資料時會呼叫get
            get(){
                Dep.target && dep.subs.push(Dep.target)
                return value;
            },

            //當設定資料時會呼叫set
            set: (newValue)=>{
                if(newValue != value){
                    //將新資料設定成響應式
                    this.observer(newValue);
                    value = newValue;
                    //當資料發生改變時,通知觀察者
                    dep.notify();
                }
            }
        }) 
    }
}
複製程式碼

總述:訂閱者是,編譯模板與資料劫持之間的橋樑,模板編譯之處新增訂閱者,並將訂閱者儲存在Dep中,在資料劫持處新增發布者,當資料發生改變的時候,通知訂閱者。

參考文件

相關文章