模擬Vue實現MVVM雙向綁定

模擬Vue實現雙向綁定

使用Vue也有一段時間了,做爲一款MVVM框架,雙向綁定是其最核心的部分,因此最近動手實現了一個簡單的雙向綁定。先上最終成果圖javascript

mvvm

思路

實現MVVM主要包含兩個方面,一個是數據變化更新視圖,另外一個則是對應的試圖變化更新數據,重點在於怎麼實現數據變了,如何去更新視圖,由於視圖更新數據使用事件監聽的形式就能夠實現,好比input標籤經過監聽input 事件就能夠實現。因此重點是如何實現數據改變動新視圖。java

實際上是經過Object.defineProperty()對屬性進行數據劫持,設置set函數,當數據改變後就回來觸發這個函數,因此要將一些須要更新的方法放在這裏面就能夠實現data更新view了。node

實現功能

  1. 實現一個解析器Compile,能夠掃描和解析每一個節點的相關指令,並根據初始化模板數據以及初始化相應的訂閱器。git

    1. 文本的編譯 例如{{message}}
    2. 指令的編譯 例如v-model
  2. 實現一個監聽器Observer,用來劫持並監聽全部屬性,若是有變更的,就通知訂閱者。
  3. 實現一個訂閱者Watcher,能夠收到屬性的變化通知並執行相應的函數,從而更新視圖。

MVVM.js 整合

class MVVM {
    constructor(options) {
        // 先把可用的東西掛載到實例上
        this.$el = options.el;
        this.$data = options.data;
        
        // 判斷有沒有要編譯的模板
        if(this.$el) {
            // 數據劫持 將對象的全部屬性,都添加 get 和 set 方法
            new Observer(this.$data)
            // 用數據和元素進行模板編譯
            new Compile(this.$el, this)
        }
    }
}

模板的編譯(compile.js)

class Compile {
    constructor(el, vm) {
        // 判斷el是否是元素節點
        this.el = this.isElementNode(el) ? el : document.querySelector(el); 
        this.vm = vm;
        if(this.el) {
            // 1. 先把真實的DOM移入到內存中(fragment),提升性能
            let fragment = this.node2fragment(this.el)
            // 2. 編譯 -> 提取想要的元素節點 v-model 和 文本節點 {{}}
            this.compile(fragment)
            // 3. 把fragment塞回頁面
            this.el.appendChild(fragment)
        }
    }
    
    // 對fragment進行編譯
    compile(fragment) {
        let childNodes = fragment.childNodes;
         Array.from(childNodes).forEach( node => {
            // 遍歷fragment的元素節點
             if(this.isElemenrNode(node)) {
                 // 是元素節點,須要深度遞歸檢查
                 this.compile(node)
                 // 編譯元素
                 this.compileElement(node)
             } else {
                 // 是文本節點,編譯文本
                 this.compileText(node)
             }
         })
    }
}

將數據進行劫持,添加get 和 set方法

class Observer {
    constructor(data) {
        this.observe(data)
    }
    observe(data) {
        // 要對data數據的全部屬性都改成set 和 get 的形式
        if(!data || typeof data === 'object') {
            return ;
        }
        // 取出對象 key 值
        Object.keys(data).forEach( key => {
            // 數據劫持
            this.defineReactive(data, key, data[key]);
            this.observe(data[key]); // 遞歸劫持
        })
    }
    
    // 定義響應式(數據劫持)
    defineReactive(obj, key, value) {
        let that = this;
        Object.defineProperty(obj, key, {
            enumerable: true, // 可枚舉
            configurable: true, // 屬性可以被改變
            get() { // 取值時調用的方法
                return value;
            },
            set(newVal) { // 當給data屬性中設置值的時候,更改獲取的屬性的值
                if(newVal !== value) {
                   value = newVal;
                    that.observe(newVal); // 若是是對象修改繼續劫持
                }
            }
        })
    }
}

觀察者(watcher.js)

最後,給須要變化的元素添加一個觀察者,經過觀察者監聽數據變化以後執行對應的方法。github

class Watcher {
    constructor (vm, expr, cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // 先獲取一下老值
        this.value = this.get()
    }
    getVal() {
        // 獲取實例上對應的數據
        expr = expr.split('.');
        return expr.reduce( (prev, next) => {
            return prev[next];
        }, vm.$data)
    }
    get() {
        let value = this.getVal(this.vm, this.expr);
         return value;
    }
    // 對外暴露的方法,老值和新值比對,若是變化
    update() {
        let newVal = this.getVal(this.vm, this.expr);
        let oldVal = this.value;
        if(newVal !== oldVal) {
            this.cb(newVal); // 對應watch的callback
        }
    }
}

Watch 完成,須要new一下調用,首先須要在模板編譯的時候須要調用,在compile.js數組

CompileUtil = {
    getVal(vm, expr) {
        // 獲取實例上對應的數據
        expr = expr.split('.');
        return expr.reduce( (prev, next) => {
            return prev[next];
        }, vm.$data)
    },
    getTextVal(vm, expr) {
        // 獲取編譯後文本的結果 
        return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
            return this.getVal(vm, arguments[1]);
        })
    },
    text(node, vm, expr) {
        // 文本處理
        let updateFn = this.updater['textUpdater']
        /*  Wather觀察者監聽  */
        expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
            new Wathcer(vm, arguments[1], (newVal) => {
                // 若是數據變化,文本須要從新獲取依賴的數據,更新文本中的內容
                updateFn && updateFn(node, this.getTextVal(vm, expr))
            })
        })

        updateFn && updateFn(node, this.getTextVal(vm, expr))
    },
    setVal(vm, expr, value) {
        expr = expr.split('.');
        return expr.reduce( (prev, next,currentIndex) => {
            if(currentIndex === expr.length - 1) {
                return prev[next] = value;
            }
            return prev[next];
        }, vm.$data)
    },

    model(node, vm, expr) {
        // 輸入框處理
        let updateFn = this.updater['modelUpdater']
        /*  Wather觀察者監聽  */
        // 這裏應該加一個監控, 數據變化,調用watch的回調
        new Wathcer(vm, expr, (newVal) => {
            // 當值變化後會調用callback,將新值傳遞過來
            updateFn && updateFn(node, this.getVal(vm, expr));
        })
        // 給輸入框加上input事件監聽
        node.addEventListener('input', (e) => {
            let newVal = e.target.value;
            this.setVal(vm, expr, newVal)
        })      

        updateFn && updateFn(node, this.getVal(vm, expr));
    },
    updater: {
        // 文本更新
        textUpdater(node, value) {
            node.textContent = value;
        },
        // 輸入框更新
        modelUpdater(node, value) {
            node.value = value;
        }
    }
}

可是此時有一個問題,Watcher沒有地方調用,更新函數不會執行,因此此時須要一個發佈訂閱模式來調用監控者。app

class Dep {
    constructor() {
        // 訂閱的數組
        this.subs = [];
    }
    addSub(watcher) {
        this.subs.push(watcher);
    }
    notify() {
        this.subs.forEach( watcher => {
            watcher.update()
        })
    }
}

此時須要修改watcherget() 這個方法:框架

get() {
    Dep.target = this;
    let value = this.getVal(this.vm, this.expr)
    Dep.target = null;
    return value;
}

此時要獲得對象的值,須要被數據劫持攔截:mvvm

defineReactive(obj, key, value) {
    let that = this;
    let dep = new Dep();  // 每一個變化的數據,都會定義一個數組,這個數組存放全部更新的操做
    Object.defineProperty(obj, key, {
        enumerable: true, // 可枚舉
        configurable: true,
        get() { 
            // 當取值時調用的方法
            Dep.target && dep.addSub(Dep.target); // 最開始編譯的時候不會執行
            return value;
        },
        set(newVal) {
            // 當給data屬性中設置值的時候 更改獲取屬性的值
            if(newVal != value) {
                that.observe(newVal); // 若是是對象繼續劫持
                value = newVal;
                dep.notify(); // 通知全部人數據更新了
            }
        }
    });
}

此時就完成了輸入框的雙向綁定。不過此時咱們取數據是以vm.$data.msg來取到數據,理想狀況咱們是vm.msg來取到數據,爲了實現這樣的形式,咱們使用proxy進行一下代理實現:函數

proxyData(data) {
        Object.keys(data).forEach( key => {
            Object.defineProperty(this, key, {
                get() {
                    return data[key]
                },
                set(newVal) {
                    data[key] = newVal
                }
            })
        })
    }

這下咱們就能夠直接經過vm.msg = 'hello'的形式來進行改變和獲取模板數據了。

歡迎交流指正,原文地址:https://github.com/hu970804/MVVM

相關文章
相關標籤/搜索