Vue MVVM 模式 解析

前言

对于学习前端的朋友,Vue框架应该是耳熟能详的。Vue成为如今最火的框架,其MVVM模式也是让大家十分喜爱,本文仅解析其原理,并不是Vue的基础使用教学。
本文源码下载:https://github.com/li-car-fei/Vue-MVVM-Model

结构

先给出代码的结构,以及MVVM模式示例图
在这里插入图片描述
在这里插入图片描述

mvvm.js

在mvvm.js中,定义了mvvm类,即对应vue类,新建该实例,则对应于我们新建vue实例 new Vue()
新建mvvm实例对象时,我们需要对数据data,计算属性computed进行数据劫持,通过defineProperty对传入的属性进行响应式绑定。
然后调用observe(),对属性进行解析,给每个属性绑定一个Dep,然后调用compile(),对模板进行解析,把命令都解析出来并且给每一个命令添加一个watcher,监听属性变化。

//新建MVVM实例对象的构造函数
function MVVM(options) {
    this.$options = options || {};                    // 拿到所有 el data methods 等
    var data = this._data = this.$options.data;
    var me = this;

    // 数据代理
    // 实现 vm.xxx -> vm._data.xxx

    //获取data中属性名数组
    Object.keys(data).forEach(function (key) {
        //对属性名数组每一个属性名,进行数据绑定
        me._proxyData(key);
    });

    this._initComputed();                                //计算属性绑定

    observe(data, this);                                 //调用observe函数,对data中所有层次的属性通过数据劫持实现数据绑定

    this.$compile = new Compile(options.el || document.body, this)                      //模板解析,初始化显示
}

//MVVM的原型对象定义
MVVM.prototype = {
    constructor: MVVM,
    $watch: function (key, cb, options) {
        new Watcher(this, key, cb);
    },

    // data proxy
    _proxyData: function (key, setter, getter) {
        var me = this;
        setter = setter ||
            Object.defineProperty(me, key, {
                //与me._data中数据通过getter和setter绑定
                configurable: false,
                enumerable: true,
                get: function proxyGetter() {
                    return me._data[key];                             // proxy代理,使得可以直接通过 this.key 的形式修改值
                },
                set: function proxySetter(newVal) {                     //vm中的setter告诉data中的setter更新数据,data中的setter再告诉监视者更新代码
                    me._data[key] = newVal;                 // proxy代理,将新设的值传到 me._data
                }
            });
    },

    _initComputed: function () {
        var me = this;
        var computed = this.$options.computed;                  // 获取 computed 对象 所有计算属性
        if (typeof computed === 'object') {
            Object.keys(computed).forEach(function (key) {
                //computed属性遍历绑定getter和setter
                Object.defineProperty(me, key, {
                    //判断此computed属性是不是只有getter
                    get: typeof computed[key] === 'function'
                        ? computed[key]                        //该属性只有getter,直接调用定义的该getter
                        : computed[key].get,                   //该属性有setter和getter,在对象中,调用对象中的getter
                    set: function () { }
                });
            });
        }
    }
};

observer.js

在observer.js中,我们看到,对属性进行遍历劫持过程中,defineReactive()方法是最为关键的,它对每一个属性进行劫持分析并且深度递归。而在defineProperty之前给属性绑定dep作为监视者,在set中通过dep.notify()通知watcher完成响应式的功能。同时,在get中Dep.target的判断也十分重要,这一步是后面dep与watcher进行关联的关键。

//新建Observe实例
function Observer(data) {                         //观察者,观察data中所有数据
    this.data = data;
    this.walk(data);                             //开始观察
}

Observer.prototype = {
    constructor: Observer,
    walk: function (data) {
        var me = this;                                    //this指向Observe的实例
        //获取data对象中所有属性名数组,遍历调用convert
        Object.keys(data).forEach(function (key) {                     // 将data中所有值进行观察
            me.convert(key, data[key]);
        });
    },
    convert: function (key, val) {
        //对传入的属性名,对应的属性值调用defineReactive响应性数据绑定
        this.defineReactive(this.data, key, val);
    },

    defineReactive: function (data, key, val) {
        // 创建属性对应的dep对象
        var dep = new Dep();
        //嵌套遍历子对象,若无子对象则返回
        var childObj = observe(val);               //递归调用,实现data中全部层次的数据劫持

        Object.defineProperty(data, key, {
            enumerable: true, // 可枚举
            configurable: false, // 不能再define重新定义
            get: function () {                         //返回值,建立dep与watcher之间的关系
                if (Dep.target) {                             // watcher中调用getter获取值前绑定了Dep.target,
                    dep.depend();                    //建立关系
                }
                return val;
            },
            set: function (newVal) {
                if (newVal === val) {                         //如果新值与旧值一样,不作响应,返回
                    return;
                }
                val = newVal;
                // 新的值是object的话,进行嵌套监听
                childObj = observe(newVal);
                // 通知订阅者
                dep.notify();
            }
        });
    }
};

function observe(value, vm) {
    //被观察的必须是对象
    if (!value || typeof value !== 'object') {
        return;
    }

    return new Observer(value);                     //新建Observe实例
};


var uid = 0;

function Dep() {
    this.id = uid++;                            //没创建一个实例都id加一
    this.subs = [];                           //多个订阅者(监听)的数组
}

Dep.prototype = {
    //增加watcher监听
    addSub: function (sub) {
        this.subs.push(sub);                         // 在watcher添加此dep时调用,此dep也添加那个watcher
    },

    //去建立dep和watcher之间的关系
    depend: function () {
        Dep.target.addDep(this);                      // 这里的 Dep.target 指向 watcher ,调用watcher的addDep方法,把此dep添加到watcher的dep数组中
    },

    //移除watcher监听
    removeSub: function (sub) {
        var index = this.subs.indexOf(sub);
        if (index != -1) {
            this.subs.splice(index, 1);
        }
    },

    //遍历监听者watcher列表,通知更新值
    notify: function () {
        this.subs.forEach(function (sub) {
            sub.update();                              // 调用watcher 的update方法,更细视图
        });
    }
};

Dep.target = null;

compile.js

compile解析文档过程中,通过正则判断,node节点类型判断的方式,从文档流之中解析出vue指令,然后给每一个vue指令绑定一个watcher,在watcher之中有对应的更新视图的函数对应不同的更新方法,如css样式更新,html内容更新,{{}}内容更新等

//解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令替换数据,以及绑定相应的更新函数

function Compile(el, vm) {
    this.$vm = vm;
    //判断是否是节点,通过document.querySelector拿到第一个符合el选择器的元素的节点
    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);
    }
}

Compile.prototype = {
    constructor: Compile,
    node2Fragment: function (el) {
        //创建文档片段
        var fragment = document.createDocumentFragment(),
            child;

        // 将原生节点拷贝添加到fragment
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }

        return fragment;
    },

    init: function () {
        this.compileElement(this.$fragment);
    },

    compileElement: function (el) {
        //获取文档片段的子节点
        var childNodes = el.childNodes,
            me = this;

        //先通过slice方法将childNodes转为数组,再遍历            可以用 [...childNodes] 代替
        [].slice.call(childNodes).forEach(function (node) {
            //获取子节点的text内容
            var text = node.textContent;
            //{{}}-内容绑定 的正则判断式
            var reg = /\{\{(.*)\}\}/;

            if (me.isElementNode(node)) {              //判断子节点是不是元素节点
                me.compile(node);                     //是元素节点,对其添加的键列进行处理

            } else if (me.isTextNode(node) && reg.test(text)) {           //判断是不是文本节点并且文本有 {{}} 数据获取
                me.compileText(node, RegExp.$1.trim());
                //对文本的 {{}} 数据获取进行处理,并将正则表达式匹配到的第一个子匹配字符串(绑定的值)传过去
                // RegExp.$1 获得正则匹配第一个匹配的值,即是{{}}内的值
            }

            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);                                //继续嵌套遍历其子节点,进行上面判断
            }
        });
    },

    compile: function (node) {
        var nodeAttrs = node.attributes,                        //获取元素节点的attr键
            me = this;

        //先通过slice方法将nodeAttrs键列转为数组,再遍历          可以用 [...nodeAttrs] 代替
        [].slice.call(nodeAttrs).forEach(function (attr) {
            var attrName = attr.name;                        //获取绑定的键名
            if (me.isDirective(attrName)) {                 //判定键名是否以 v- 开头
                var exp = attr.value;                     //获取绑定的键值,字符串形式
                var dir = attrName.substring(2);          //substring字符串方法提取键名 v- 后面的内容
                // 事件指令
                if (me.isEventDirective(dir)) {                //判断键名是不是 v-on 开头
                    compileUtil.eventHandler(node, me.$vm, exp, dir);               //添加事件监听
                } else {      // 普通指令
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);        //普通指令处理
                }

                node.removeAttribute(attrName);                 // 删除已经处理的键attribute
            }
        });
    },

    compileText: function (node, exp) {
        compileUtil.text(node, this.$vm, exp);                    //对文本节点存在的 {{}} 数据获取的处理,exp即匹配到的要获取的值
    },

    isDirective: function (attr) {
        return attr.indexOf('v-') == 0;
    },

    isEventDirective: function (dir) {
        return dir.indexOf('on') === 0;
    },

    isElementNode: function (node) {
        return node.nodeType == 1;
    },

    isTextNode: function (node) {
        return node.nodeType == 3;
    }
};

// 非事件指令处理集合
var compileUtil = {
    text: function (node, vm, exp) {                      //  v-text
        this.bind(node, vm, exp, 'text');
    },

    html: function (node, vm, exp) {                       //  v-html
        this.bind(node, vm, exp, 'html');
    },

    model: function (node, vm, exp) {                      //  v-model
        this.bind(node, vm, exp, 'model');

        var me = this,
            val = this._getVMVal(vm, exp);
        node.addEventListener('input', function (e) {        //  添加input事件监听,实现v-model的双向数据绑定
            var newValue = e.target.value;                 //事件触发时,先获得新值
            if (val === newValue) {
                return;                                    //新值与旧值相同,返回
            }

            me._setVMVal(vm, exp, newValue);
            val = newValue;
        });
    },

    class: function (node, vm, exp) {                         //  v-class
        this.bind(node, vm, exp, 'class');
    },

    bind: function (node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];                       //获取对应的添加数据的对应方法

        updaterFn && updaterFn(node, this._getVMVal(vm, exp));           //先获取具体绑定的值,再调用添加数据的方法

        new Watcher(vm, exp, function (value, oldValue) {            //新建watcher实例,exp对应的数据改变时,调用回调函数
            updaterFn && updaterFn(node, value, oldValue);          //回调函数的作用与上相似,先获取具体绑定的值,再调用添加数据的方法
        });
    },

    // 事件处理
    eventHandler: function (node, vm, exp, dir) {
        var eventType = dir.split(':')[1],                       //获取绑定的具体事件名
            fn = vm.$options.methods && vm.$options.methods[exp];              //查看methods中是否有对应的处理函数

        if (eventType && fn) {
            node.addEventListener(eventType, fn.bind(vm), false);            //添加事件绑定
        }
    },

    //获取具体的绑定的值                    获取 vm.data 中的指定的那个值
    _getVMVal: function (vm, exp) {
        var val = vm;
        exp = exp.split('.');              //将匹配到的js表达式先分割
        exp.forEach(function (k) {           //遍历
            val = val[k];                            //让val最终获得到实际绑定的值
        });
        return val;
    },

    //将改变的新的值,设为绑定的数据的新值      v-model中调用    更新 vm.data 中的值
    _setVMVal: function (vm, exp, value) {
        var val = vm;
        exp = exp.split('.');              //将匹配到的js表达式先分割
        exp.forEach(function (k, i) {       //遍历
            // 非最后一个key,更新val的值
            if (i < exp.length - 1) {           //还没遍历到最后一层,继续
                val = val[k];
            } else {                            //遍历到最后一层,设立新值
                val[k] = value;
            }
        });
    }
};

//添加具体数据到元素节点的多种对应方法:
var updater = {
    textUpdater: function (node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;                    //将要获取的值添加到元素节点的text节点中,完成数据获取
    },

    htmlUpdater: function (node, value) {                                               //将要获取的值添加到元素节点的innerHTML中,完成数据获取
        node.innerHTML = typeof value == 'undefined' ? '' : value;
    },

    classUpdater: function (node, value, oldValue) {                           //将要获取的值添加到元素节点的class属性中,完成双向数据获取
        var className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');

        var space = className && String(value) ? ' ' : '';

        node.className = className + space + value;
    },

    modelUpdater: function (node, value) {                                    //将要获取的值添加到元素节点的value属性中,完成双向数据绑定
        node.value = typeof value == 'undefined' ? '' : value;
    }
};

watcher.js

最后我们来看watcher.js中对watcher的定义。在new watcher 新建watcher实例时,势必要去获取observer劫持了的属性的值,以完成第一次视图渲染,而在此时将Dep.target赋值为当前的watcher,如前面所说,在observer劫持的属性的get中判断Dep.target,如果有指向,则完成两者的绑定

//Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图

function Watcher(vm, expOrFn, cb) {
    this.cb = cb;                                        //回调函数
    this.vm = vm;
    this.expOrFn = expOrFn;                              //绑定的键值,字符串形式
    this.depIds = {};                                  //这个watcher所有相关联dep的容器对象

    if (typeof expOrFn === 'function') {
        this.getter = expOrFn;                               // 绑定的是函数,则直接赋给getter,用以调用函数获取值
    } else {
        this.getter = this.parseGetter(expOrFn.trim());           // 绑定的是表达式,trim()去除前后空格
    }

    this.value = this.get();                          // 得到表达式的初始值
}

Watcher.prototype = {
    constructor: Watcher,
    update: function () {                  // 属性更改,视图更新
        this.run();
    },
    run: function () {
        var value = this.get();            // 先调用getter获取新的值
        var oldVal = this.value;               // 旧的值,绑定在初始化时的 this.value 中的
        if (value !== oldVal) {
            this.value = value;                  // 把储存老的值的 this.value 赋值新的值
            this.cb.call(this.vm, value, oldVal);      //调用回调函数更新界面                   
        }
    },
    addDep: function (dep) {
        //判断dep与watcher的关系是否已经建立
        if (!this.depIds.hasOwnProperty(dep.id)) {
            dep.addSub(this);                          // 调用dep中的addSub方法 给dep添加当前这个watcher      用于更新
            this.depIds[dep.id] = dep;                   // 给watcher添加关联的dep
        }
    },
    get: function () {
        // 给Dep指定当前的watcher
        Dep.target = this;
        // 获取函数或者表达式的值,内部调用get建立dep与watcher的关系
        var value = this.getter.call(this.vm, this.vm);
        // 去除Dep中指定的当前watcher
        Dep.target = null;
        return value;
    },

    // 解析绑定的表达式 
    parseGetter: function (exp) {
        if (/[^\w.$]/.test(exp)) return;                        // 没有层级,直接返回, 可以通过 this.exp 获取到值

        var exps = exp.split('.');                               // 变为值层次化的数组 [person.name] => [person,name]

        return function (obj) {
            for (var i = 0, len = exps.length; i < len; i++) {               // 遍历获取深层的值
                if (!obj) return;
                obj = obj[exps[i]];
            }
            return obj;
        }
    }
};

结语

此文对Vue MVVM 模式做简要分析,并未使用Vue源码
欢迎大家一起交流学习:
编者github地址:传送
qq:1073490398
wechat:carfiedfeifei

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值