基於ES5`defineProperty` 實現簡單的 Mvvm框架

dev1發表於2019-02-17

基於ES5defineProperty 實現簡單的 Mvvm框架

PREPARE

現階段前端三大主流框架react,vue, angular都屬於 MVVM範疇,即 模型—檢視—檢視模型

採用資料驅動, 即監聽資料改變,渲染view。

核心是監聽資料的變更!

其中React使用的是 diff 演算法來實現資料變更檢測的;

Angular 則使用的是zone.js實現資料變更檢測;

Vue則使用Object.defineProperty, 後期版本則使用Object.Proxy

本文參考Vue 使用 Object.defineProperty實現資料變更檢測, 實現一個簡單的 mvvm框架

INIT

1.下面是一個簡單的類Vue元件的實現方式
  • html

    <div id="app">
        <h1>{{song}}</h1>
        <p>{{singer.a.b}}</p>
        <p>{{age}}</p>
        <input type="text" v-model="age" />
    </div>
    複製程式碼
  • javascript

     let mvvm = new Mvvm({
               el: `#app`,
               data: {
                   song: 2,
                   singer: {
                       a: {
                           b: 1
                       },
                       c: 1
                   },
                   age: 55
               }
           })
    複製程式碼
  • 首先是一個 Mvvm類,接受兩個引數(後期會加入method等引數): eldata

2. 定義Mvvm
function Mvvm(options = {}) {
    
    /*定義類的$option屬性,_data私有屬性,並將 私有$option.data的引用複製給私有屬性_data和區域性變  量data
     */
    this.$options = options;
    let data = this._data = this.$options.data;
    
    /*將data中所有的key設定觀察者,增加資料變更的檢測*/
    observe(data);
    
    /*將data中的所有的key代理到Mvvm例項上,形成mvvm例項樹,方便書寫*/
    for (let key in data) {
        Object.defineProperty(this, key, {
            configurable: true,
            get() {
                return this._data[key];
            },
            set(newVal) {
                this._data[key] = newVal;
            }
        })
    }
    
    /*資料如果變更,執行dom渲染*/
    new Compile(this.$options.el, this)
}
複製程式碼
3.看一下 observe 方法:
function observe(data) {
    if (!data || typeof data !== `object`) return;
    return new Observe(data);
}
複製程式碼

增加了一個data型別檢測機制(實際上為了遞迴結尾判斷),實際上執行 Observe類的例項化

#####4. Observe

function Observe(data) {
    
    /*建立一個訊息訂閱釋出池類,包含一個watcher屬性,watcher指向一個Watcher的例項,每個Watcher例項  都是一個訂閱者,另一個屬性是一個事件池陣列events*/    
    let dep = new Dep();
    
    /*data的每一個鍵值對迴圈執行observe方法*/
    for (let key in data) {
        let val = data[key]
        observe(val);
        
        /*對data的每一個key進行資料攔截, 設定get,當建立一個Watcher例項的時候,顯示觸發get,此時將這個Watcher例項(訂閱者)新增到訊息訂閱釋出池的事件池中;設定set 當設定新的值時候,觸發 set方法, 如果所設值與原來不相等, 則重新監聽新的值得變更, 並觸發Watcher例項(訂閱者)的notify方法,觸發 Watcher例項的回撥,更新檢視資料*/
        Object.defineProperty(data, key, {
            configurable: true,
            get() {
                Dep.watcher && dep.addEvent(Dep.watcher);
                return val;
            },
            set(newVal) {
                if (val === newVal) {
                    return;
                }
                val = newVal;
                observe(newVal);
                dep.notify();
            }
        })
    }
}
複製程式碼
5.訊息訂閱釋出池Dep
  • 類:

    function Dep() {
        this.watcher = null;
        this.events = [];
    }
    複製程式碼
  • 例項:

    Dep.prototype = {
        addEvent(event) {
            this.events.push(event);
        },
    
        notify() {
            this.events.forEach(event => event.update())
        }
    }
    複製程式碼

#####6. 訂閱者Watcher

  • /*Watcher接收三個引數,第一個是整個mvvm例項樹物件, 第二個是html中的插值表示式{{song.a.b}}, fn為設定新值之後的會調*/
    function Watcher(vm, exp, fn) {
        this.fn = fn;
        this.vm = vm;
        this.exp = exp;
        // 將watcher的例項賦值給Dep的watcher屬性,方便呼叫,而不用傳參
        Dep.watcher = this;
        
        // 進行一次取值操作, 顯示觸發mvvm例項樹某個key的get方法,從而將 Dep.watcher 新增到事件池中
        // 將如song.a.b以點號分割成陣列arr,將mvvm例項樹的引用賦值給區域性變數val
        let arr = exp.split(`.`);
        let val = vm;
        // 迴圈arr 取song.a.b的值
        arr.forEach(key => {
            val = val[key]
        });
        // 新增完之後,釋放 Dep.watcher
        Dep.watcher  = null;
    }
    複製程式碼
  • 例項

    
    /*Watcher例項的update方法會從mvvm例項樹上取出exp所對應的值,並觸發fn回撥,渲染檢視*/
    Watcher.prototype.update = function () {
        let arr = this.exp.split(`.`);
        let val = this.vm;
        arr.forEach(key => {
            val = val[key]
        });
        this.fn(val);
    }
    複製程式碼
7.Compile類

Compile類用於將所選的el元素節點 賦值給mvvm例項樹,並轉為createDocumentFragment 文件片段, 之後所需要替換的文字節點進行正則匹配並替換,之後將新的文件片段統一新增到el元素節點中。

關於文件碎片可以 在這裡 瞭解

function Compile(el, vm) {
    /*獲取元素節點*/
    vm.$el = document.querySelector(el);
    
    /*建立文件碎片物件*/
    let fragment = document.createDocumentFragment();
    
    /*當vm.$el.firstChild存在時,將vm.$el.firstChild依次加入到文件碎片中*/
    while (child = vm.$el.firstChild) {
        fragment.appendChild(child);
    }
    
    /*對html文件中的插值表示式等進行替換*/
    function replace(frag) {
        
       
        Array.from(frag.childNodes).forEach(node => {
            
            // 插值表示式
            let txt = node.textContent;
            let reg = /{{(.*?)}}/gif (node.nodeType === 1 && reg.test(txt)) {
                let arr = RegExp.$1.split(`.`);
                let val = vm;
                arr.forEach(key => {
                    val = val[key]
                });
                // 執行替換
                node.textContent = txt.replace(reg, val).trim();
                
                // 新增監聽
                new Watcher(vm, RegExp.$1, (newVal) => {
                    node.textContent = txt.replace(reg, newVal).trim();
                })
            }
            
            // 雙向資料繫結
            if(node.nodeType ===1 ) {
                let nodeAttr = node.attributes;
                // console.log(nodeAttr)  Map

                Array.from(nodeAttr).forEach(attr => {
                    let name = attr.name;
                    let value = attr.value;

                    if (name.includes(`v-`)) {
                        
                        let arr = value.split(`.`);
                        let val = vm;
                        arr.forEach( key => {
                            val = val[key]
                        })
                        console.log(value)
                        node.value = val
                    }

                    new Watcher(vm, value, (newVal) => {
                        node.value = newVal;
                    });

                    node.addEventListener(`input`, e => {
                        // 根據傳入的繫結值的物件深度值來處理, 如果是單個值,則直接賦值, 如果是多個,則使用eval()函式處理
                        if(value.split(`.`).length > 1) {  
                            eval("vm."+ value + "= e.target.value");
                        } else {
                            vm[value] = e.target.value
                        }                        
                    })
                })
            }

            if (node.childNodes && node.childNodes.length) {
                replace(node)
            }
        })
    }

    replace(fragment);
    vm.$el.appendChild(fragment);
}
複製程式碼

LAODED

以上是一個簡單的mvvm框架的實現,當然defineProperty還是有一些問題,比如說對應陣列的變更檢測是辦不到,而Proxy的出現則解決了這類問題,有時間的話,大家可以試試基於Proxy去實現一套簡單的mvvm框架。

相關文章