手把手教你实现MVVM架构

658 阅读2分钟

引言

现在的前端真可谓是百花齐放,百家争鸣,各种框架层出不穷,但是主要目前用的最多的还是要数VueReact、以及Angular,这三种,当然不乏近期新出的一些其他框架,但是她们都有一个显著的特点,那就是使用了MVVM的架构。

首先我们要搞清楚什么是MVVM

MVVM就是Model-View-ViewModel的缩写,MVVM最早由微软提出来,它借鉴了桌面应用程序的MVC思想,在前端页面中,把Model用纯JavaScript对象表示,View负责显示,两者做到了最大限度的分离。把ModelView关联起来的就是ViewModelViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model

改变JavaScript对象的状态,会导致DOM结构作出对应的变化!这让我们的关注点从如何操作DOM变成了如何更新JavaScript对象的状态,而操作JavaScript对象比DOM简单多了。

这就是MVVM的设计思想:关注Model的变化,让MVVM框架去自动更新DOM的状态,从而把开发者从操作DOM的繁琐步骤中解脱出来!

接下来我会带着你们如何去实现一个简易的MVVM架构。

一、构建MVVM构造函数

创建一个MVVM构造函数,用于接收参数,如:datamethodscomputed等:

function MVVM(options) { 
    this.$options = options; 
    let data = this._data = this.$options.data; 
    observe(data); 
    for (let key in data) { 
        Object.defineProperty(this, key, { 
            enumerable: true, 
            get() { 
                return this._data[key]; 
            }, 
            set(newVal) { 
                this._data[key] = newVal; 
            } 
        }); 
    }; 
    initComputed.call(this); 
    new Compile(options.el, this); 
    options.mounted.call(this); 
}

二、构建Observe构造函数

创建一个Observe构造函数,用于监听数据变化:

function Observe(data) { 
    let dep = new Dep(); 
    for (let key in data) { 
        let val = data[key]; 
        observe(val); 
        Object.defineProperty(data, key, { 
            enumerable: true, 
            get() { 
                Dep.target && dep.addSub(Dep.target); 
                return val; 
            }, 
            set(newVal) { 
                if (val === newVal) { 
                    return; 
                } 
                val = newVal; 
                observe(newVal); 
                dep.notify(); 
            } 
        }); 
    };
};

三、构建Compile构造函数

创建一个Compile构造函数,用于解析模板指令:

function Compile(el, vm) { 
    vm.$el = document.querySelector(el); 
    let fragment = document.createDocumentFragment(); 
    while (child = vm.$el.firstChild) { 
        fragment.appendChild(child); 
    } 
    replace(fragment);
    function replace(frag) {
        Array.from(frag.childNodes).forEach(node => { 
            let txt = node.textContent; 
            let reg = /\{\{(.*?)\}\}/g; 
            if (node.nodeType === 3 && 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; 
            Array.from(nodeAttr).forEach(attr => { 
                let name = attr.name; 
                let exp = attr.value; 
                if (name.includes('v-')) { 
                    node.value = vm[exp]; 
                } 
                new Watcher(vm, exp, newVal => { 
                    node.value = newVal; 
                }); 
                node.addEventListener('input', e => { 
                    let newVal = e.target.value; 
                    vm[exp] = newVal; 
                }); 
             }); 
         };
         if (node.childNodes && node.childNodes.length) { 
             replace(node); 
         } 
       }); 
     }
     vm.$el.appendChild(fragment); 
}

四、构建Watcher构造函数

创建一个Watcher构造函数,用于更新视图:

function Watcher(vm, exp, fn) { 
    this.fn = fn; 
    this.vm = vm; 
    this.exp = exp; 
    Dep.target = this; 
    let arr = exp.split('.'); 
    let val = vm; 
    arr.forEach(key => { 
        val = val[key]; 
    }); 
    Dep.target = null; 
} 

Watcher.prototype.update = function() { 
        let arr = this.exp.split('.'); 
        let val = this.vm; 
        arr.forEach(key => { 
            val = val[key]; 
        }); 
        this.fn(val); 
}

五、构建Dep构造函数

创建一个Dep构造函数,用于管理Watcher:

function Dep() { 
    this.subs = []; 
} 

Dep.prototype.addSub = function(sub) { 
    this.subs.push(sub); 
} 

Dep.prototype.notify = function() { 
    this.subs.forEach(sub => { 
        sub.update(); 
    }); 
}

六、构建initComputed构造函数

创建一个initComputed构造函数,用于初始化计算属性:

function initComputed() { 
    let vm = this; 
    let computed = this.$options.computed; 
    Object.keys(computed).forEach(key => { 
        Object.defineProperty(vm, key, { 
            get: typeof computed[key] === 'function' ? computed[key] : computed[key].get, 
            set() {} 
        }); 
    }); 
}

总结:

至此我们就完成了一个简易的MVVM框架,虽然简易,但是基本的核心思想差不多都已经表达出来了,最后还是希望大家不要丢在收藏文件夹里吃灰,还是要多多动手练习一下,所谓眼过千遍,不如手过一遍。