300行代碼手寫簡單vue.js,徹底弄懂MVVM底層原理


當我們對vue的用法較為熟練的時候,但有時候在排查bug的時候還是會有點迷惑。主要是因為對vue各種用法和各種api使用都是只知其然而不知其所以然。這時候我們想到可以去看看源碼,但是源碼太長,其實我們只要把大概實現流程實現一遍,很多開發中想不明白的地方就會豁然開朗。下面我們就來實現一個簡單的vue.js

vue采取數據劫持,配合觀察者模式,通過Object.defineProperty() 來劫持各個屬性的setter和getter,在數據變動時,發布消息給依賴收集器dep,去通知觀察者,做出對應的回調函數,去更新視圖。(也就是在getter中收集依賴,在setter中通知依賴更新。)

其實vue主要就是整合Observer,compile和watcher三者,通過Observer來監聽 model數據變化表,通過compile來解析編譯模板指令,最終利用Watcher搭起observer 和compile的通信橋梁,達到數據變化=>視圖變化,視圖變化=>數據變化的雙向綁定效果。

下面來一張圖↓

 這個流程圖已經非常形象深刻的表達了vue的運行模式,當你理解了這個流程,再去看vue源碼時就會容易很多了

聲明一下,下面的代碼只簡單實現了vue里的

  1. v-model(數據的雙向綁定)
  2. v-bind/v-on
  3. v-text/v-html
  4. 沒有實現虛擬dom,采用文檔碎片(createDocumentFragment)代替
  5. 數據只劫持了Object,數組Array沒有做處理

代碼大致結構如下,初步定義了6個類

 代碼如下,具體操作案例可以看==>GitHub

// 定義Vue類
class Vue {
    constructor(options) {
        // 把數據對象掛載到實例上
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options;
        // 如果有需要編譯的模板
        if (this.$el) {
            // 數據劫持 就是把對象的所有屬性 改成get和set方法
            new Observer(this.$data);
            // 用數據和元素進行編譯
            new Compiler(this.$el, this);
            // 3. 通過數據代理實現 主要給methods里的方法this直接訪問data
            this.proxyData(this.$data);
        }
    }
    //用vm代理vm.$data 
    proxyData(data){
        for(let key in data){
            Object.defineProperty(this,key,{
                get(){
                    return data[key];
                },
                set(newVal){
                    data[key] = newVal;
                }
            })
        }
    }
}

// 編譯html模板
class Compiler {
    // vm就是vue對象
    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);
            // console.log(fragment);
            // 2.編譯 => 提取想要的元素節點 v-model和文本節點{{}}
            this.compile(fragment);
            // 3.把編譯好的fragment再放到頁面里
            this.el.appendChild(fragment);
        }
    }
    
    /* 一些輔助方法 */
    isElementNode(node) {
        return node.nodeType === 1;
    }
    isDirective(name) { // 判斷是不是指令
        return name.includes('v-');
    }
    isEventName(attrName){ // 判斷是否@開頭
        return attrName.startsWith('@');
    }
    isBindName(attrName){ // 判斷是否:開頭
        return attrName.startsWith(':');
    }
    
    /* 核心方法區 */
    node2fragment(el){ // 需要將el中的內容全部放到內存中
        // 文檔碎片
        let fragment = document.createDocumentFragment();
        let firstChild;
        while(firstChild = el.firstChild){
            fragment.appendChild(firstChild);
        }
        return fragment; // 內存中的節點
    }
    compile(fragment){
        // 1.獲取子節點
        let childNodes = fragment.childNodes;
        // 2.遞歸循環編譯
        [...childNodes].forEach(node=>{
            if(this.isElementNode(node)){
                this.compileElement(node); // 這里需要編譯元素
                this.compile(node); // 是元素節點,還需要繼續深入的檢查
            }else{
                // 文本節點
                // 這里需要編譯文本
                this.compileText(node);
            }
        });
    }
    compileElement(node){ // 編譯元素
        // 帶v-model v-html ...
        let attrs = node.attributes; // 取出當前節點的屬性
        // attrs是類數組,因此需要先轉數組
        [...attrs].forEach(attr=>{
            // console.log(attr); // type="text" v-model="content" v-on:click="handleclick" @click=""...
            let attrName = attr.name; // type v-model v-on:click @click
            if(this.isDirective(attrName)){ // 判斷屬性名字是不是包含v-
                // 取到對應的值放到節點中
                let expr = attr.value; // content/變量 handleclick/方法名
                // console.log(expr)
                let [, type] = attrName.split('-'); // model html on:click
                let [compileKey, detailStr] = type.split(':'); // 處理 on: bind:
                // node this.vm.$data expr
                CompileUtil[compileKey](node, this.vm, expr, detailStr);
                // 刪除有指令的標簽屬性 v-text v-html等,普通的value等原生html標簽不必刪除
                node.removeAttribute('v-' + type);
            }else if(this.isEventName(attrName)){ // 如果是事件處理 @click='handleClick'
                let [, detailStr] = attrName.split('@');
                CompileUtil['on'](node, this.vm, attr.value, detailStr);
                // 刪除有指令的標簽屬性
                node.removeAttribute('@' + detailStr);
            }else if(this.isBindName(attrName)){ // 如果是:開頭,動態綁定值
                let [, detailStr] = attrName.split(':');
                CompileUtil['bind'](node, this.vm, attr.value, detailStr);
                // 刪除有指令的標簽屬性
                node.removeAttribute(':' + detailStr);
            }
        })
    }
    compileText(node){ // 編譯文本
        // 帶{{}}
        let expr = node.textContent; // 取文本中的內容
        let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}}
        if(reg.test(expr)){
            // node this.$data
            // console.log(expr); // {{content}}
            CompileUtil['text'](node, this.vm, expr);
        }
    }
}

// 編譯模版具體執行
const CompileUtil = {
    getVal(vm, expr){ // 獲取實例上對應的數據
        expr = expr.split('.'); // [animal,dog]/[animal,cat]
        return expr.reduce((prev, next)=>{ // vm.$data.
            return prev[next];
        }, vm.$data)
    },
    // 這里實現input輸入值變化時 修改綁定的v-model對應的值
    setVal(vm, expr, inputValue){ // [animal,dog]
        let exprs = expr.split('.'), len = exprs.length;
        exprs.reduce((data,currentVal, idx)=>{
            if(idx===len-1){
                data[currentVal] = inputValue;
            }else{
                return data[currentVal]
            }
        }, vm.$data)
    },
    getTextVal(vm, expr){ // 獲取編譯文本后的結果
        return expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
            // console.log(args); // ["{{title}}", "title", 0, "{{title}}"]
            // ["{{ animal.dog }}", " animal.dog ", 0, "{{ animal.dog }}-vs-{{ animal.cat }}"]
            return this.getVal(vm, args[1].trim());
        });
    },
    text(node, vm, expr){ // 文本處理
        let updateFn = this.updater['textUpdater'];
        // {{content}} => "welcome to animal world"
        let value;
        if(expr.indexOf('{{')!==-1){ // dom里直接寫{{}}的時候
            value = this.getTextVal(vm, expr);
            // {{a}} {{b}} 對多個值進行監控
            expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
                new Watcher(vm, args[1].trim(), ()=>{
                    // 如果數據變化了,文本節點需要重新獲取依賴的屬性更新文本中的內容
                    updateFn && updateFn(node, this.getTextVal(vm, expr));
                })
            });
        }else{ // v-text 的時候
            value = this.getVal(vm, expr);
            new Watcher(vm, expr, (newVal)=>{
                // 當值變化后會調用cb 將新值傳遞過來
                updateFn && updateFn(node, newVal);
            });
        }
        updateFn && updateFn(node, value);
    },
    html(node, vm, expr) { // 
        let updateFn = this.updater['htmlUpdater'];
        updateFn && updateFn(node, this.getVal(vm, expr));
    },
    model(node, vm, expr){ // 輸入框處理
        let updateFn = this.updater['modelUpdater'];
        // console.log(this.getVal(vm, expr)); // "welcome to animal world"
        // 這里應該加一個監控 數據變化了  應該調用這個watch的callback
        new Watcher(vm, expr, (newVal)=>{
            // 當值變化后會調用cb 將新值傳遞過來
            updateFn && updateFn(node, newVal);
        });
        // 視圖 => 數據 => 視圖
        node.addEventListener('input', (e)=>{
            this.setVal(vm, expr, e.target.value);
        })
        updateFn && updateFn(node, this.getVal(vm, expr));
    },
    on(node, vm, expr, detailStr) {
        let fn = vm.$options.methods && vm.$options.methods[expr];
        node.addEventListener(detailStr, fn.bind(vm), false);
    },
    bind(node, vm, expr, detailStr){
        // v-bind:src='...' => href='...'
        node.setAttribute(detailStr, expr);
    },
    updater:{
        // 文本更新
        textUpdater(node, value){
            node.textContent = value;
        },
        // html更新
        htmlUpdater(node, value){
            node.innerHTML = value;
        },
        // 輸入框更新
        modelUpdater(node, value){
            node.value = value;
        }
    }
}

// 觀察者
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 dep = new Dep();
        // 在獲取某個值的時候
        Object.defineProperty(obj, key, {
            enumerable: true, // 可枚舉
            configurable: true, // 可修改
            get(){ // 當取值的時候
                // 訂閱數據變化時,往Dev中添加觀察者
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            // 采用箭頭函數在定義時綁定this的定義域
            set: (newVal)=>{ // 更改data里的屬性值的時候
                if(value === newVal) return;
                this.observe(newVal); // 如果設置新值是對象,劫持
                value = newVal;
                // 通知watcher數據發生改變
                dep.notify();
            }
        })
    }
}

// 觀察者的目的就是給需要變化的那個元素增加一個觀察者,當數據變化后執行對應的方法
class Watcher{
    constructor(vm, expr, cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // 先獲取一下老的值
        this.oldVal = this.getOldVal();
    }
    // 獲取實例上對應的老值
    getOldVal(){
        // 在利用getValue獲取數據調用getter()方法時先把當前觀察者掛載
        Dep.target = this;
        const oldVal = CompileUtil.getVal(this.vm, this.expr);
        // 掛載完畢需要注銷,防止重復掛載 (數據一更新就會掛載)
        Dep.target = null;
        return oldVal;
    }
    // 對外暴露的方法 通過回調函數更新數據
    update(){
        const newVal = CompileUtil.getVal(this.vm, this.expr);
        if(newVal !== this.oldVal){
            this.cb(newVal); // 對應watch的callback
        }
    }
}

// Dep類存儲watcher對象,並在數據變化時通知watcher
class Dep{
    constructor(arg) {
        // 訂閱的數組
        this.subs = []
    }
    addSub(watcher){
        this.subs.push(watcher);
    }
    notify(){ // 數據變化時通知watcher更新
        this.subs.forEach(w=>w.update());
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM