MVVM極易理解版

Meteor發表於2018-08-13

寫在前面

前段時間面試,MVVM原理成為了一道必考題。由於理解不夠深,最近詳細瞭解以結構圖流程分析原理。

一原型圖解

MVVM極易理解版

使用MVVM雙向繫結

  • 定義雙向繫結,傳入元素,和資料。
var vm = new MVVM({
    el: '#mvvm-app',
    data: {
        someStr: 'hello ',
        className: 'btn',
        htmlStr: '<span style="color: #f00;">red</span>',
        child: {
            someStr: 'World !'
        }
    }
});
複製程式碼

MVVM類

  • 新建劫持資料
  • 編譯繫結資料
class MVVM {
    constructor(options) {
        this.$options = options || {};
        var data = this._data = this.$options.data;
        Object.keys(data).forEach(key => {
            this._proxyData(key);
        })
        //資料劫持
        observe(data, this);
        //編譯
        this.$compile = new Compile(options.el || document.body, this);
    }
    _proxyData(key, setter, getter) {
        Object.defineProperty(this, key, {
            configurable: false,
            enumerable: true,
            get: function proxyGetter() {
                return this._data[key];
            },
            set: function proxySetter(newVal) {
                this._data[key] = newVal;
            }
        })
    }
}
複製程式碼

Compile類

  • 將真實DOM移動到虛擬DOM中
  • 解析元素中的指令
  • 指令新建訂閱傳入更新函式
class Compile {
    constructor(el, vm) {
        this.$vm = vm;
        this.$el = this.isElementNode(el) ? el : document.querySelector(el);
        if (this.$el) {
            //生成文件碎片
            this.$fragment = this.node2Fragment(this.$el);
            //編譯
            this.init()
            //文件碎片加回容器中
            this.$el.appendChild(this.$fragment);
        }
    }
    node2Fragment(el) {
        var fragment = document.createDocumentFragment(),
            child;
        while (child = el.firstChild) {
            fragment.appendChild(child);         
        };
        return fragment;
    }
    init() {
        this.compileElement(this.$fragment);
    }
    compileElement(el) {        

        var childNodes = el.childNodes;

        [].slice.call(childNodes).forEach(node => {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;
            
            if(this.isElementNode(node)){
                //指令解析
                this.compile(node);
            }else if(this.isTextNode(node) && reg.test(text)){
                this.compileText(node, RegExp.$1)
            }

            if(node.childNodes && node.childNodes.length){
                this.compileElement(node);
            }
        })
    }
    compile(node){
        var nodeAttrs = node.attributes;

        [].slice.call(nodeAttrs).forEach(attr => {
            var attrName = attr.name;
            
            if(this.isDirective(attrName)){
                var exp = attr.value;
                var dir = attrName.substring(2);
                //事件指令
                
                if(this.isEventDirective(dir)){
                    compileUtil.eventHandler(node, this.$vm, exp, dir);
                }else{
                    compileUtil[dir] && compileUtil[dir](node, this.$vm, exp);
                }
                node.removeAttribute(attrName);
            }
        })
    }
    isDirective(attr){
        return attr.indexOf('v-') === 0;
    }
    isEventDirective(attr){
        return attr.indexOf('on') === 0;
    }
    isElementNode(node) {
        return node.nodeType == 1;
    }
    isTextNode(node) {
        return node.nodeType == 3;
    }
    compileText(node, exp) {
        compileUtil.text(node, this.$vm, exp);
    }
}

//指令處理集合
var compileUtil = {
   ...
}

var updater = {
   ...
}
複製程式碼

Observer類

  • 劫持資料
  • 資料變化通知Watcher
//資料劫持
class Observer {
    constructor(data) {
        this.data = data;
        this.walk(data);
    }
    walk(data) {
        Object.keys(data).forEach(key => {
            this.convert(key, data[key]);
        })
    }
    convert(key, val) {
        this.defineReactive(this.data, key, val);
    }
    //繫結資料,新增發布訂閱,核心**
    defineReactive(data, key, val) {
        var dep = new Dep();
        var childObj = observe(val);
        Object.defineProperty(data, key, {
            enumerable: true, //可列舉
            configurable: false, //不能再define
            get: function(){
                if(Dep.target){
                    console.log(Dep.target, 'Dep.target');
                    dep.depend();
                }
                return val;
            },
            set: function(newVal){             
                if(newVal === val){
                    return;
                }                
                val = newVal;
                // 新的值object的話,進行監聽
                childObj = observe(newVal);
                console.log(newVal);
                //通知訂閱者
                dep.notify();
            }
        })
    }
}

function observe(value, vm) {
    if (!value || typeof value !== 'object') {
        return;
    }
    return new Observer(value);
}

複製程式碼

Dep類

  • 釋出訂閱類
var uid = 0;
class Dep {
    constructor() {
        this.id == uid++;
        this.subs = [];
    }
    addSub(sub) {
        this.subs.push(sub);
    }
    depend() {
        Dep.target.addDep(this)
    }
    removeSub(sub) {
        this.subs.remove(sub);
    }
    notify() { 
        this.subs.forEach(sub => {
            sub.update();
        })
    }
}

Dep.target = null;
複製程式碼

Watcher類

  • 監控資料變化,釋出訊息,執行訂閱函式。
class Watcher{
    constructor(vm, expOrFn, cb){
        this.cb = cb;
        this.vm = vm;
        this.expOrFn = expOrFn;
        this.depIds = {};
        
        if(typeof expOrFn === 'function') {
            this.getter = expOrFn;
        }else{
            this.getter = this.parseGetter(expOrFn);
        }
        this.value = this.get();
    }
    update(){
        this.run();
    }
    run(){
        var value = this.get();
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    }
    get(){
        Dep.target = this;
        var value = this.getter.call(this.vm, this.vm);
        Dep.target = null;
        return value;
    }
    parseGetter(exp){
        if(/[^\w.$]/.test(exp)) return;
        var exps = exp.split(',');
        return function(obj) {
            for (let i = 0; i < exps.length; i++) {
                if(!obj) return;
                obj = obj[exps[i]];
            }
            return obj;
        }
    }
    addDep(dep){
        if (!this.depIds.hasOwnProperty(dep.id)) {
            dep.addSub(this);
            this.depIds[dep.id] = dep;
        }
    }
}
複製程式碼

相關文章