1. 程式人生 > >JavaScript 實現一個簡單的MVVM前端框架(ES6語法)

JavaScript 實現一個簡單的MVVM前端框架(ES6語法)

模仿 image 詳細 發布 regexp doc eof bject htm

前言

隨著前端各大框架的崛起,為我們平時的開發帶來了相當的便利,我們不能一直停留在應用層面,今天就自己動手實現一個乞丐版的MVVM小框架

完整代碼github地址

效果

技術分享圖片

技術分享圖片

html代碼

<div id="app">
    <p>{{a}}</p>
    <p>{{b.b}}</p>
    <input type="text" v-model="a">
</div>

js調用代碼

const vm = new Mvvm({
        el: '#app',
        data: {
            a: 1,
            b: { b : 2 }
        }
    })

基本是模仿vue的調用方式

實現步驟

  1. 數據劫持Observe
  2. 數據代理(讓Mvvm對象可以處理數據)
  3. 模板編譯Compile
  4. 發布訂閱
  5. 視圖與數據進行關聯
  6. 實現雙向數據綁定

代碼分析

// 定義框架類名Mvvm,我們則可以直接實例化new Mvvm() 來調用
class Mvvm {
    constructor(options){
        /**
         * options 則是前臺傳來的數據
         * {
                el: '#app',
                data: {
                    a: 1,
                    b: { b : 2 }
                }
            }
         */
        const {el,data} = options;
        this._data = data;
        Observe.observeData(data); // 通過該函數把所有前臺傳來的data中的數據劫持
        this.mount(data); // 把所有的data數據代理到this,也就是Mvvm對象上
        Mvvm.compile(el,this); // 解析模板數據,也就是解析HTML中的{{a}} {{b.b}}
    }
    // 把data中的數據掛載到this上
    mount(data){
        // 遍歷data數據 通過defineProperty進行重新創建屬性到this上
        for(let key in data){
            Object.defineProperty(this,key,{
                enumerable:true, // 可枚舉
                get(){
                    return this._data[key];
                },
                set(newVal){
                    this._data[key] = newVal;
                }
            })
        }
    }

    // 解析模板功能
    static compile(el,_that){
        new Compile(el,_that);
    }
}
// 對數據進行劫持
class Observe{

    constructor(data){
        this.deepObserve(data);
    }

    deepObserve(data){
        let dep = new Dep(); // 創建一個可觀察對象
        for(let key in data){
            let value = data[key];
            Observe.observeData(value); // 遞歸調用數據劫持方法
            this.mount(data,key,value,dep); // 數據劫持主體方法
        }
    }

    mount(data,key,value,dep){
        // 其實就是把data中的數據一層層遞歸的通過defineProperty方式創建
        Object.defineProperty(data,key,{
            enumerable:true,
            get(){
                Dep.target && dep.addSub(Dep.target); //Dep.target = watcher 這個存在的時候,添加到可觀察對象數組中
                return value; // get返回該值
            },
            set(newVal){
                // 當設置值時,新值老值進行比對
                if(newVal === value){
                    return;
                }
                value = newVal;
                Observe.observeData(newVal);// 把後來手動設置的值也劫持了
                dep.notify(); // 發布所有的訂閱來更新界面數據
            }
        })
    }

    static observeData(data){
        // 遞歸的終止條件,這裏寫的並不完善!! 我們主要目的還是理解mvvm
        if(typeof data !== 'object'){
            return ;
        }
        return new Observe(data);
    }
}

class Compile{
    constructor(el,vm){
        // vm = this
        vm.$el = document.querySelector(el);
        // 創建一個文檔片段
        let fragment = document.createDocumentFragment();
        let child;
        while(child = vm.$el.firstChild){
            // 不斷遍歷DOM,添加到文檔片段(內存)中
            fragment.appendChild(child);
        }
        // replace是解析HTML的核心函數
        this.replace(fragment,this,vm);
        // 把更新後的文檔片段插入回DOM,達到更新視圖的目的
        vm.$el.appendChild(fragment);
    }
    // 解析DOM
    replace(fragment,that,vm){
        // 循環文檔片段中的DOM節點
        Array.from(fragment.childNodes).forEach(function (node) {
            let text = node.textContent; // 節點值
            let reg = /\{\{(.*)\}\}/; // 正則匹配{{}}裏面的值
            // nodeType === 3 表示文本節點
            if(node.nodeType === 3 && reg.test(text)){
                let arr = RegExp.$1.split('.'); // RegExp.$1獲取到 b.b , 並通過.轉換成數組
                let val = vm; // val 指針指向 vm對象地址
                arr.forEach(function (k) {
                    val = val[k]; // vm['b'] 可以一層層取到值
                });
                // 給這個node創建一個watcher對象,用於後期視圖動態更新使用
                new Watcher(vm,RegExp.$1,function (newVal) {
                    node.textContent = text.replace(reg,newVal);
                });
                // 更新視圖 {{a}} ==> 1
                node.textContent = text.replace(reg,val);
            }
            // 元素節點
            if(node.nodeType === 1){
                let nodeAttrs = node.attributes; // 獲取DOM節點上的屬性列表
                // 遍歷該屬性列表
                Array.from(nodeAttrs).forEach((attr)=>{
                    let name = attr.name; // 獲取屬性名 v-model
                    let exp = attr.value; // 獲取屬性值 "a"
                    if(name.startsWith('v-')){
                        node.value = vm[exp]; // 實現了把a的值添加到input輸入框內
                    }
                    // 給該node創建一個watcher對象,用於動態更新視圖
                    new Watcher(vm,exp,function (newVal) {
                        node.value = newVal; // 更新輸入框的值
                    });
                    // 輸入框添加事件
                    node.addEventListener('input',function (e) {
                        // 會調用數據劫持中的set方法,從而觸發 dep.notify()發布所有的訂閱來更新界面數據
                        vm[exp] = e.target.value;
                    },false);
                })
            }
            // 遞歸解析DOM節點
            if(node.childNodes){
                that.replace(node,that,vm);
            }
        });
    }
}

// 簡單的發布訂閱
class Dep{
    constructor(){
        this.subs = [];
    }
    addSub(sub){
        this.subs.push(sub);
    }
    notify(){
        this.subs.forEach(sub=>{
            sub.update();
        })
    }
}

// Watcher對象,用來跟node的關聯起來。把後期需要更新的node變成Watcher對象,存入內存中
class Watcher{
    constructor(vm,exp,fn){
        this.vm = vm; // this對象
        this.exp = exp; // 值
        this.fn = fn; // 回調函數
        Dep.target = this; // 發布訂閱對象Dep,添加一個屬性target = this 也是當前watcher
        let val = vm;
        let arr = exp.split('.');
        arr.forEach(function (k) {
            val = val[k]; // 這個步驟會循環的調用改對象的get,所以就會把該watcher添加到觀察數組中
        });
        Dep.target = null;
    }
    // 給每個watcher都加一個update方法,用來發布
    update(){
        // 通過最新 this對象,去取到最新的值,觸發watcher的回調函數,來更新node節點中的數據,以達到更新視圖的目的
        let val = this.vm;
        let arr = this.exp.split('.');
        arr.forEach(function (k) {
            val = val[k];
        });
        this.fn(val); // 這個傳入的val就是最新計算出來的值
    }
}

小結

代碼已經全部寫了詳細的註釋,但是可能還是會有難以理解的地方,這個時候多動手練練,使用下可能會讓你更加熟悉MVVM原理

JavaScript 實現一個簡單的MVVM前端框架(ES6語法)