Vue之實現MVVM

MVVM

MVVM是指Model-View-ViewModeljavascript

  • Model:模型
  • View: 視圖,可組件化
  • ViewModel: 抽象視圖,集成了數據綁定引擎,實現View和Model的雙向綁定

Vue的運行機制

初始化流程

  1. 建立vue實例對象
  2. init過程當中初始化生命週期,初始化事件,初始化渲染,執行beforeCreate周期函數,初始化datapropscomputedwatcher,執行create周期函數
  3. 初始化後,調用$mount方法對vue實例進行掛載,包括模板編譯,渲染,更新
  4. 若是定義了template,則須要進行編譯:將template字符串編譯爲render function
  5. 調用$mountmountComponent方法,先執行beforeCreate周期函數,實例化一個渲染watcher,在它的回調函數(初始化及數據變化時執行)中調用updateComponent方法。
  6. 調用render方法將render function渲染成虛擬dom
  7. 生成虛擬DOM樹後,調用update方法,update方法會調用pacth方法把虛擬DOM轉換成真正的DOM節點

響應式流程

  1. init時會調用Object.defineProperty方法監聽實例的數據變化(get和set方法),從而實現數據劫持。
  2. 在初始化的編譯階段,會讀取vue實例中與視圖相關的響應式數據,get函數會進行訂閱收集(把監聽watcher實例放到訂閱者Dep的數組sub中),這是數據劫持和訂閱發佈模式就造成了ViewModel
  3. 數據或視圖變化時,會觸發數據劫持的set方法,set會通知Dep中相應的watcherwatcher調用update方法來更新視圖。

MVVM實現思路

MVVM雙向數據綁定原理是經過 數據劫持+發佈訂閱 實現的。html

經過Object.defineProperty()來給對象的屬性添加get,set方法,在數據變更時觸發相應的監聽回調。vue

Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。java

要實現一個MVVM的思路爲:node

  • 實現一個數據劫持Observe,給對象添加get,set方法,監聽到數據變更時,通知訂閱者(Watcher)。get用來收集訂閱,set用來派發更新。
  • 實現一個解析編譯Compile,對每一個元素節點進行匹配,綁定和替換{{}}的內容
  • 實現一個監聽Watcher,鏈接Observe和Compile,收到數據變更的通知,執行相應回調函數,更新視圖
  • 實現一個消息訂閱Dep,用一個數組來收集訂閱者,數據變更觸發notify,再調用訂閱者的update方法
  • 實現一個Vue入口函數

具體實現

vue.js數組

//Vue構造函數
function Vue(option = {}) {
    this.$option = option;
    let data = this._data = this.$option.data;
    observe(data); // 數據劫持
    // 數據代理,簡化data數據的寫法,如vue._data.name變成vue.name
    for(let key in data) {
        Object.defineProperty(this, key, {
            configurable: true,
            get() {
                return this._data[key];
            },
            set(newVal) {
                this._data[key] = newVal;
            }
        })
    }
    //初始化computed,將this指向實例
    initComputed.call(this);
    // 數據編譯,解析{{}}的內容
    new Compile(option.el, this);
    //執行mounted鉤子函數
    option.mounted.call(this);
}

//數據劫持就是給對象增長get,set
function Observe(data) {
    let dep =new Dep();
    for(let key in data) {
        let val = data[key];
        observe(val) //遞歸繼續向下,實現深度的數據劫持
        // Object.defineProperty定義對象的屬性
        Object.defineProperty(data, key, {
            configurable: true, // 能夠配置對象,刪除屬性
            get() {
                Dep.target && dep.addSub(Dep.target); //將watcher實例添加到訂閱事件中
                return val
            },
            set(newVal){ //修改值的時候
                if(val == newVal) { //值相同就不理
                    return;
                }
                val = newVal; 
                observe(newVal); //把新值也定義成屬性
                dep.notify(); //執行watcher中的update方法
            }
        })
    }
}

//遞歸函數
function observe(data) {
    if(!data || typeof data != 'object') return;
    return new Observe(data);
}

//編譯函數
function Compile(el, vm){
    vm.$el = document.querySelector(el); // 將el掛載到實例上
    let fragment = document.createDocumentFragment(); // 建立一個新的空白文檔片斷
    while(child = vm.$el.firstChild) { //將el的內容都拿到,放入內存中,節省開銷
        fragment.appendChild(child);        
    }   
    //替換內容 
    function replace(frag){
        Array.from(frag.childNodes).forEach(node => {
            let txt = node.textContent;
            let reg = /\{\{(.*?)\}\}/g; // 正則匹配{{}}
            if(node.nodeType === 1 && reg.test(txt)) { //既是文本節點又是大括號{{}}
               function replaceTxt() {
                   node.textContent = txt.replace(reg, (matched, placholder) => {
                       //placholder匹配到的分組,name,age
                       new Watcher(vm, placholder, replaceTxt); // 監聽數據變化,替換{{}}的內容
                       return placholder.split('.').reduce((val, key) => { //reduce爲數組的每一個元素依次執行回調函數
                           return val[key]; //將vm的數據傳給val作初始值
                       }, vm)
                   })
               }
               replaceTxt();
            }

            //實現雙向綁定
            if(node.nodeType === 1) {
                let nodeAttr = node.attributes; //獲取元素上的屬性,類數組
                Array.from(nodeAttr).forEach(attr => {
                    let name = attr.name; // v-model type
                    let exp = attr.value; // c
                    if(name.includes('v-')){
                        node.value = vm[exp]; // 將vm中的c的值,掛載到節點上
                    }
                    //監聽數據變化
                    new Watcher(vm, exp, function(newVal){
                        node.value = newVal;
                    })
                    node.addEventListener('input', e => {
                        let newVal = e.target.value;
                        //給vm中的值賦值
                        vm[exp] = newVal;
                    })
                })
            }
            //子節點
            if(node.childNodes && node.childNodes.length) {
                replace(node);
            }
        })
    }
    replace(fragment);
    vm.$el.appendChild(fragment);
}

//發佈訂閱,把函數放入數組就是訂閱,發佈就是讓函數執行
function Dep(){
    this.subs = [];
}
Dep.prototype.addSub = function(sub) {
    this.subs.push(sub);
}
Dep.prototype.notify = function() {
    this.subs.forEach(sub => sub.update());
}

//監聽函數,給這個類建立的實例,添加update方法
function Watcher(vm, exp, fn){
    this.fn = fn; //將fn放到實例上
    this.vm = vm;
    this.exp = exp;
    // 定義一個屬性,target是Dep的一個靜態屬性,是一個全局watcher,dep其實是對watcher的一種管理
    Dep.target = this;
    let arr = exp.split('.');
    let val = vm;
    arr.forEach(key => {
        val = val[key]; //獲取值的時候調用get()方法
    })
    Dep.target = null;
}
Watcher.prototype.update = function() {
    // 值已經修改,再經過vm,exp來獲取新的值
    let arr = this.exp.split('.');
    let val = this.vm;
    arr.forEach(key => {
        val = val[key]; //經過get()獲取到新的值
    })
    this.fn(val); //fn爲替換{{}}的內容
    
}

//實現Computed
function initComputed() {
    let vm = this;
    let computed = this.$option.computed; // 從option上拿到computed屬性
    Object.keys(computed).forEach(key => {
        Object.defineProperty(vm, key, {
            // 判斷computed的key是對象仍是函數,若是是函數會調get方法,若是是對象,手動調get方法
            // sum獲取a,b的值會調get方法
            get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
            set() {}
        })
    })
}
複製代碼

測試:app

<div id="mvvm">
  <p>{{name}}</p>
  <p>{{age}}</p>
  <input type="text" v-model='c'/>
  <p>{{c}}</p>
</div>

<script> let mvvm = new Vue({ el:'#mvvm', data: { name: '小明', age: 20, a: 10, b: 30, c: '' }, computed: { sum() { return this.a + this.b }, noop() {} }, mounted() { setTimeout(() => { console.log('完成'); }, 1000); } }) </script>
複製代碼

參考:dom

juejin.im/post/5abdd6…mvvm

juejin.im/post/5cd8a7…函數

相關文章
相關標籤/搜索