欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Vue MVVM 模式 解析

程序员文章站 2022-03-30 23:05:44
...

前言

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

结构

先给出代码的结构,以及MVVM模式示例图
Vue MVVM 模式 解析
Vue 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

相关标签: js vue