手寫MVVM

let vm = new MVVM({
            el:'#app',
            data:{
                message:{
                    a: 'hello zfpx',
                },
                a:1
            }
        })
複製代碼

vue中初始化一個vue實例是這樣用的,那麼MVVM內部原理到底是怎麼實現的呢?vue

MVVM實現圖示以下 node

如圖主要分爲5部分:MVVM類,Compile, Observer,Dep,Watcher

1. MVVM類實現數組

MVVM類主要功能:bash

  1. 存掛載的dom元素以及data
  2. 代理$data上的屬性到this實例上
  3. 調用Observer類數據劫持
  4. 將掛載的東西傳入Compile類,編譯模板
class MVVM{
    constructor(options){
        // 先把可用的東西掛載在實例上
        this.$el = options.el;
        this.$data = options.data;

        // 若是有要編譯的模板,就開始編譯
        if(this.$el){
            // 數據劫持 就是把對想的全部屬性 改爲get和set方法
            new Observer(this.$data); 
            //將$data代理到this上
            this.proxyData(this.$data);
            // 用數據和元素進行編譯
            new Compile(this.$el, this);
        }
    }
    proxyData(data){
        Object.keys(data).forEach(key=>{
            Object.defineProperty(this,key,{
                get(){
                    return data[key]
                },
                set(newValue){
                    data[key] = newValue
                }
            })
        })
    }
}

複製代碼

2. Observer類實現 Observer類主要功能:app

  1. 將vue中data的每一個屬性都改爲劫持形式(set和get的形式),深度遞歸data對象的全部屬性
  2. 每一個屬性都對應一個數組(Dep類實現的方法),這個數組存放全部更新的操做
class Observer{
    constructor(data){
       this.observe(data);
    }
    observe(data){
        // 要對這個data數據將原有的屬性改爲set和get的形式
        if(!data || typeof data !== 'object'){
            return;
        }
        // 要將數據 一一劫持 先獲取取到data的key和value
        Object.keys(data).forEach(key=>{
            // 劫持
            this.defineReactive(data,key,data[key]);
            this.observe(data[key]);// 深度遞歸劫持
        });
    }
    // 定義響應式
    defineReactive(obj,key,value){
        let that = this;
        // 每一個屬性 都會對應一個數組,這個數組是存放全部更新的操做
        let dep = new Dep(); 
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get(){ 
    // 當取值時,將Dep.target(watcher即set屬性時,要執行的回調函數)推入數組
                Dep.target && dep.addSub(Dep.target); 
                return value;
            },
            set(newValue){ 
               // 當給data屬性中設置值的時候 更改獲取的屬性的值
                if(newValue!=value){
                    // 這裏的this不是實例
                    // 若是是對象繼續劫持 
                    // 好比你this.$data = {a: '2'}換成了一個新對象
                    that.observe(newValue);
                    value = newValue;
                    dep.notify(); // 通知全部watcher數據更新了
                }
            }
        });
    }
}
複製代碼

3. Dep類實現 Dep類的主要功能dom

  1. 訂閱,發佈更新通知
class Dep{
    constructor(){
        // 訂閱的數組
        this.subs = []
    }
    addSub(watcher){
        this.subs.push(watcher);
    }
    notify(){
        this.subs.forEach(watcher=>watcher.update());
    }
}
複製代碼

4.watcher類實現函數

watcher功能ui

  1. 給每個屬性添加一個觀察者,當屬性變化時,執行對應的更新操做
class Watcher{
// vm: vm實例 expr: {{message.a}}中的message.a cb:屬性更新後的回調函數
    constructor(vm,expr,cb){
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // 先獲取一下老的值,進入get方法,將watcher實例賦給Dep.target
        //而後 執行this.getval()方法時,會讀取data對象上的屬性
        // 一旦讀取屬性(數據劫持)
        // 就會執行Observer類中的defineReactive方法
        // 執行這裏:Dep.target && dep.addSub(Dep.target); 
        //將watcher實例推入訂閱的數組
        this.value = this.get(); // 讀取老值 
    }
     // 獲取data對象上對應的expr屬性值
    getVal(vm, expr) {
        expr = expr.split('.'); // [message,a]
        return expr.reduce((prev, next) => { // vm.$data.a
            return prev[next];
        }, vm.$data);
    }
    get(){
        Dep.target = this;
        let value = this.getVal(this.vm,this.expr);
        Dep.target = null;
        return value;
    }
    // 對外暴露的方法
    //一旦給data對象的屬性設置值,就會執行Observer類defineReactive方法中的
    // dep.notify()--》執行訂閱的數組中的watcher隊列的update方法
    update(){
        let newValue = this.getVal(this.vm, this.expr);
        let oldValue = this.value;
        if(newValue != oldValue){
            this.cb(newValue); // 對應watch的callback
        }
    }
}
// 用新值和老值進行比對 若是放生變化 就調用更新方法
複製代碼

5. Compile類實現 Compile類功能this

  1. 把真實dom移入內存 fragment
  2. 編譯:結合模板和數據進行編譯,主要處理v-*, {{}},等其餘vue特有的東西
  3. 把編譯好的fragment塞回到頁面裏去
class Compile {
    constructor(el, vm) {
        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);
        }
    }
    /* 專門寫一些輔助的方法 */
    isElementNode(node) {
        return node.nodeType === 1;
    }
    // 是否是指令
    isDirective(name) {
        return name.includes('v-');
    }
    /* 核心的方法 */
    compileElement(node) {
        // 帶v-model v-text
        let attrs = node.attributes; // 取出當前節點的屬性
        Array.from(attrs).forEach(attr => {
            // 判斷屬性名字是否是包含v-model
            let attrName = attr.name;
            if (this.isDirective(attrName)) {
                // 取到對應的值放到節點中
                let expr = attr.value;
                let [, type] = attrName.split('-');
                // node this.vm.$data expr
                CompileUtil[type](node, this.vm, expr);
            }
        })
    }
    compileText(node) {
        // {{}}的處理
        let expr = node.textContent; // 取文本中的內容
        let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}} {{c}}
        if (reg.test(expr)) {
            // node this.vm.$data text
            CompileUtil['text'](node, this.vm, expr);
        }
    }
    compile(fragment) {
        // 遞歸
        let childNodes = fragment.childNodes;
        Array.from(childNodes).forEach(node => {
            if (this.isElementNode(node)) {
                // 是元素節點,還須要繼續深刻的檢查
                // 這裏須要編譯元素
                this.compileElement(node);
                this.compile(node)
            } else {
                // 文本節點
                // 這裏須要編譯文本
                this.compileText(node);
            }
        });
    }
    node2fragment(el) { 
        // 須要將el中的內容所有放到內存中
        // 文檔碎片 內存中的dom節點
        let fragment = document.createDocumentFragment();
        let firstChild;
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment; // 內存中的節點
    }
}

CompileUtil = {
    getVal(vm, expr) { // 獲取實例上對應的數據
        expr = expr.split('.'); // [message,a]
        return expr.reduce((prev, next) => { // vm.$data.a
            return prev[next];
        }, vm.$data);
    },
    getTextVal(vm, expr) { 
    // 獲取編譯文本後的結果 {{message.a}}--> message.a-->data.message.a
        return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
            return this.getVal(vm, arguments[1]);
        })
    },
    text(node, vm, expr) { // 文本處理
        let updateFn = this.updater['textUpdater'];
        // {{message.a}} => hello,zfpx;
        let value = this.getTextVal(vm, expr);
        // {{a}} {{b}}
        expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
        //每一個屬性都對應一個watcher
            new Watcher(vm, arguments[1],(newValue)=>{
                // 若是數據變化了,文本節點須要從新獲取依賴的屬性更新文本中的內容
                updateFn && updateFn(node,this.getTextVal(vm,expr));
            });
        })
        updateFn && updateFn(node, value)
    },
    setVal(vm,expr,value){ // [message,a]
        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'];
        // 這裏應該加一個監控 數據變化了 應該調用這個watch的callback
        new Watcher(vm,expr,(newValue)=>{
            // 當值變化後會調用cb 將新的值傳遞過來 ()
            updateFn && updateFn(node, this.getVal(vm, expr));
        });
        node.addEventListener('input',(e)=>{
            let newValue = e.target.value;
            this.setVal(vm,expr,newValue)
        })
        updateFn && updateFn(node, this.getVal(vm, expr));
    },
    updater: {
        // 文本更新
        textUpdater(node, value) {
            node.textContent = value
        },
        // 輸入框更新
        modelUpdater(node, value) {
            node.value = value;
        }
    }
}
複製代碼
相關文章
相關標籤/搜索