探索MVVM实现原理

1,066 阅读4分钟

本文通过github上的一个MVVM框架github.com/DMQ/mvvm作探索记录,与vue的双向数据绑定有很多类似的思想可以帮助理解vue。

该MVVM框架实现的原理图:

    <div id="app">
        <h1>{{student.name}}</h1>
        <button v-on:click="changeData">change student.name</button>
    </div>
    <script>
        const vm = new MVVM({
            el: '#app',
            data: {
                student: { name:'jerry' }
            },
            methods: {
                changeData(){
                    this.student.name = 'lisa'
                }
            }
        });
    </script>

初始化视图阶段

在实例化vm实例时首先进行通过_proxyData()应用Object.defineProperty实施数据代理,配置对象中this可以访问到data的数据,通过initComputed()简单代理compute计算属性

function MVVM(options) {
    // 保存option到vm实例
    this.$options = options || {};
    // 保存data对象为_data到vm实例
    var data = this._data = this.$options.data;
    // 保存vm为me
    var me = this;

    // 数据代理
    // 实现 vm.xxx -> vm._data.xxx
    // 遍历data对象得到每个key,调用实例方法_proxyData(key),为每个data上的属性创建<访问器属性,get和set>与_data对象的属性联动,这些存取器都是定义在vm实例对象上的,完成了<数据代理>
    Object.keys(data).forEach(function(key) {
        me._proxyData(key);
    });

    // <初始computed>,跟上面数据代理很相像,遍历了computed对象,并在vm实例对象上绑定<访问器属性,get>与computed的属性联动,(未见访问器属性set的设置)
    this._initComputed();

    observe(data, this);

    // 编译模板
    this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
    constructor: MVVM,
    $watch: function(key, cb, options) {
        new Watcher(this, key, cb);
    },

    _proxyData: function(key, setter, getter) {
        var me = this;
        setter = setter || 
        Object.defineProperty(me, key, {
            configurable: false,
            enumerable: true,
            get: function proxyGetter() {
                return me._data[key];
            },
            set: function proxySetter(newVal) {
                me._data[key] = newVal;
            }
        });
    },

    _initComputed: function() {
        var me = this;
        var computed = this.$options.computed;
        if (typeof computed === 'object') {
            Object.keys(computed).forEach(function(key) {
                Object.defineProperty(me, key, {
                    get: typeof computed[key] === 'function' 
                            ? computed[key] 
                            : computed[key].get,
                    set: function() {}
                });
            });
        }
    }
};

observe(data, this)开始数据劫持阶段

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

/* 
    数据劫持与监听器:劫持监听所有数据的变化
*/
function Observer(data) {
    // 将vm实例的data对象,绑定一份在劫持者对象中,显然这是堆区中的同一个data对象
    this.data = data;
    // 进行数据劫持
    this.walk(data);
}

Observer.prototype = {
    constructor: Observer,
    walk: function (data) {
        var me = this;
        // 遍历劫持数据对象 的 所有属性和值 -> 定义扩展属性访问器get、set来劫持对象
        Object.keys(data).forEach(function (key) {
            me.convert(key, data[key]);
        });
    },
    convert: function (key, val) {
        this.defineReactive(this.data, key, val);
    },

    defineReactive: function (data, key, val) {
        // 新建一个消息订阅器
        var dep = new Dep();
        // 递归:对数据的所有层次进行数据劫持
        var childObj = observe(val);

        // 通过为每一项数据添加数据读取器get()、set()进行数据劫持
        Object.defineProperty(data, key, {
            enumerable: true, // 可枚举
            configurable: false, // 不能再define
            get: function () {
                // 当有watcher访问到该属性时
                if (Dep.target) {
                    dep.depend();
                }
                return val;
            },
            set: function (newVal) {
                // 
                if (newVal === val) {
                    return;
                }
                val = newVal;
                // 对新的值补充劫持
                childObj = observe(newVal);
                // 通知订阅者
                dep.notify();
            }
        });
    }
};


/* 
    Dep:消息订阅器
    它的结构:
    {
        id:消息订阅器的唯一标识(自动生成)

        target:一个临时指向watcher的指针,当有新的watcher创建时,这个临时指针指向新创建的watcher,并且,watcher新建时访问了一次被劫持的属性,在那个时刻watcher与dep相互绑定(watcher调用addDep方法,在watcher的depIds容器中绑定Dep,在Dep的sbus容器中添加该新建的watcher)此过程称为<添加订阅者>

        subs:[] 储存watcher的容器 
    }

    Dep的生命周期:创建、添加订阅者、通知

    从observer对数据劫持的过程可以知道:
        1.Dep是进行数据劫持的时候创建的,data对象中的属性被深度劫持,
            如果将data对象看做树状结构,那么树的每一个节点都对应创建了一个消息订阅器Dep

        2.observer的数据劫持是动态的,set()访问器中补充了对新值的劫持:observer(newVal),如果新值为一个对象则劫持它的所有属性并新建Dep

        3.Dep添加订阅者这个动作,是watcher新建的时刻进行的,数据劫持时为get()部署了绑定dep的流程(watcher新建时每访问一次get(),绑定流程就会执行一次,例如针对a.b.c这样的表达式,watcher访问了三次get,绑定了三个Dep)

    从watcher的角度也可以知道:
        1.Dep虽然为data对象树的每一个属性节点都创建了一次,但是它只绑定仅有的watcher
        2.watcher 与 Dep,显然是一种多对多的关系;一个watcher有可能同时绑定了多个Dep(表达式a.b.c),一个Dep也可能绑定着多个watcher(一个属性)
*/
var uid = 0;

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

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

    depend: function () {
        // addDep方法是watcher的方法
        Dep.target.addDep(this);
    },

    removeSub: function (sub) {
        var index = this.subs.indexOf(sub);
        if (index != -1) {
            this.subs.splice(index, 1);
        }
    },

    notify: function () {
        this.subs.forEach(function (sub) {
            sub.update();
        });
    }
};

Dep.target = null;

随后this.$compile = new Compile(options.el || document.body, this)进行模板的编译,在编译阶段针对 普通指令v-text、v-html、v-class(这个是这个框架自己的)、{{}}等实例化对应的watcher(传入一个调用updater的回调)向Dep消息订阅者添加订阅者,并监听消息订阅器通知,编译的初始化结果挂载到真实DOM上完成初始化视图显示。

/* 
    Compile模板编译器
    它上面将会挂载有
    {   $vm:mvvm实例对象,
        $el:真实DOM中的标记模板,
        $fragment:内存中的模板,用于编译
    }
*/

function Compile(el, vm) {
    this.$vm = vm;
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);

    if (this.$el) {
        // 创建fragment容器并把模板中的所有节点抛入:此时DOM被清空,view视图空白
        this.$fragment = this.node2Fragment(this.$el);
        // 进入编译器,在fragment容器中编译其中的每一个节点
        this.init();
        // 在DOM原来的位置中插入已编译好的模板,此时DOM树被构建,view视图渲染
        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.call(childNodes).forEach(function (node) {
            var text = node.textContent;

            // 该正则用于匹配{{xxx}}
            var reg = /\{\{(.*)\}\}/;

            // 编译器的核心部分:判断节点类型(元素节点 / 文本节点(需要编译'{{}}'部分))
            if (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                // 匹配{{}}
                // RegExp.$1.trim()?? 表示第一个子匹配,即{{word}}中的word
                me.compileText(node, RegExp.$1.trim());
            }

            // 如果该节点之下还有子节点,递归编译,直到编译完全部节点
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },

    /*
        编译元素节点
        主要工作:遍历属性->判断并确定指令类型(事件指令v-on / 普通指令[v-text、v-html、v-class])

     */
    compile: function (node) {
        // 获取该元素节点上所有的属性(节点)
        var nodeAttrs = node.attributes,
            me = this;

        [].slice.call(nodeAttrs).forEach(function (attr) {
            // attr:如v-on:xxx = 'exp' 、v-text='exp'

            //attrName: 如v-on:xxx 、 v-text
            var attrName = attr.name;
            if (me.isDirective(attrName)) {
                // 指令表达式
                var exp = attr.value;
                // 指令
                var dir = attrName.substring(2);

                // 判断指令类型
                if (me.isEventDirective(dir)) {
                    // 事件指令:绑定事件监听
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    // 普通指令:这里与编译文本节点{{}}相似
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }
                // 编译完成后移除该节点上的属性节点
                node.removeAttribute(attrName);
            }
        });
    },
    /*
        编译文本节点
        主要工作:调用指令处理器compileUtil,取出当前vm实例的具体属性的值,更新到虚拟DOM上
     */
    compileText: function (node, exp) {
        compileUtil.text(node, this.$vm, 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) {
        this.bind(node, vm, exp, 'text');
    },

    html: function (node, vm, exp) {
        this.bind(node, vm, exp, 'html');
    },
    // 双向数据绑定的处理:
    // 该指令的设计类似于 事件指令+普通指令
    // 相对于普通指令,它额外绑定了input事件监听,当input事件触发,调用被劫持属性的set()
    model: function (node, vm, exp) {
        this.bind(node, vm, exp, 'model');

        var me = this,
            val = this._getVMVal(vm, exp);
        node.addEventListener('input', function (e) {
            var newValue = e.target.value;
            if (val === newValue) {
                return;
            }

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

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

    // 用于指定该指令使用哪种更新方法,是指令处理器的门方法
    bind: function (node, vm, exp, dir) {
        // 更新器对象updater:
        // 包含多种更新方法,具体根据指令名['text''html''class'、’model‘]等进行匹配更新方法
        var updaterFn = updater[dir + 'Updater'];

        // 获取了更新方法并执行了更新方法
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));

        // 新建了一个观察者???-》watcher.js
        new Watcher(vm, exp, function (value, oldValue) {
            updaterFn && updaterFn(node, value, oldValue);
        });
    },

    // 在DOM上绑定事件监听
    eventHandler: function (node, vm, exp, dir) {
        var eventType = dir.split(':')[1],
            fn = vm.$options.methods && vm.$options.methods[exp];

        if (eventType && fn) {
            node.addEventListener(eventType, fn.bind(vm), false);
        }
    },

    _getVMVal: function (vm, exp) {
        var val = vm;
        // exp是指令的表达式,
        // 由于可能是word也可能是word.detail这样的形式,这里需要循环历经多次地从实例对象中取出该值
        exp = exp.split('.');
        exp.forEach(function (k) {
            val = val[k];
        });
        return val;
    },

    _setVMVal: function (vm, exp, value) {
        var val = vm;
        exp = exp.split('.');
        exp.forEach(function (k, i) {
            // 非最后一个key,更新val的值
            if (i < exp.length - 1) {
                val = val[k];
            } else {
                val[k] = value;
            }
        });
    }
};

// 多种指令下更新viwe视图的方法 对应各自的指令
var updater = {
    textUpdater: function (node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    },

    htmlUpdater: function (node, value) {
        node.innerHTML = typeof value == 'undefined' ? '' : value;
    },

    classUpdater: function (node, value, oldValue) {
        var className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');

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

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

    modelUpdater: function (node, value, oldValue) {
        node.value = typeof value == 'undefined' ? '' : value;
    }
};

watcher.js

/* 
    watcher:观察者
    在模板编译的时候,具体在bind()时刻(可见watcher对象 仅为 模板中的普通指令表达式所构建的,一个表达式对应一个watcher) 
    实例化了一个观察者 ->   (传入了vm实例对象,指令表达式,一个回调函数<操作updater进行虚拟DOM更新>),

    它的扼要结构
    {
        vm:vm实例
        cb:一个调用updater的回调函数,当Dep通知该watcher时调用
        expOrFn:指令表达式
        depIds:储存Dep的容器
        getter: 访问属性的方法,调用它会触发被劫持属性的get()
    }
*/
function Watcher(vm, expOrFn, cb) {
    // 回调函数
    this.cb = cb;
    // vm实例
    this.vm = vm;
    // 指令表达式
    this.expOrFn = expOrFn;
    // 一个储存器:储存了该watcher依赖的消息订阅器Dep
    this.depIds = {};

    // getter?
    if (typeof expOrFn === 'function') {
        this.getter = expOrFn;
    } else {
        this.getter = this.parseGetter(expOrFn.trim());
    }

    // 访问了一次data中对应属性的get(),并保存该值
    this.value = this.get();
}

Watcher.prototype = {
    constructor: Watcher,
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.get();
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    // 在depIds容器中添加消息订阅器Dep,同时在Dep中的subs[]容器中保存watcher
    addDep: function(dep) {
        // 1. 每次调用run()的时候会触发相应属性的getter
        // getter里面会触发dep.depend(),继而触发这里的addDep
        // 2. 假如相应属性的dep.id已经在当前watcher的depIds里,说明不是一个新的属性,仅仅是改变了其值而已
        // 则不需要将当前watcher添加到该属性的dep里
        // 3. 假如相应属性是新的属性,则将当前watcher添加到新属性的dep里
        // 如通过 vm.child = {name: 'a'} 改变了 child.name 的值,child.name 就是个新属性
        // 则需要将当前watcher(child.name)加入到新的 child.name 的dep里
        // 因为此时 child.name 是个新值,之前的 setter、dep 都已经失效,如果不把 watcher 加入到新的 child.name 的dep中
        // 通过 child.name = xxx 赋值的时候,对应的 watcher 就收不到通知,等于失效了
        // 4. 每个子属性的watcher在添加到子属性的dep的同时,也会添加到父属性的dep
        // 监听子属性的同时监听父属性的变更,这样,父属性改变时,子属性的watcher也能收到通知进行update
        // 这一步是在 this.get() --> this.getVMVal() 里面完成,forEach时会从父级开始取值,间接调用了它的getter
        // 触发了addDep(), 在整个forEach过程,当前wacher都会加入到每个父级过程属性的dep
        // 例如:当前watcher的是'child.child.name', 那么child, child.child, child.child.name这三个属性的dep都会加入当前watcher
        if (!this.depIds.hasOwnProperty(dep.id)) {
            dep.addSub(this);
            this.depIds[dep.id] = dep;
        }
    },
    get: function() {
        Dep.target = this;
        var value = this.getter.call(this.vm, this.vm);
        Dep.target = null;
        return value;
    },

    parseGetter: function(exp) {
        if (/[^\w.$]/.test(exp)) return; 

        var exps = exp.split('.');

        return function(obj) {
            for (var i = 0, len = exps.length; i < len; i++) {
                if (!obj) return;
                obj = obj[exps[i]];
            }
            return obj;
        }
    }
};

数据更新阶段

当一些操作致使data中的数据发生改变,访问了Observer劫持属性的set(),此时调用dep.notify()遍历dep上绑定的所有订阅者watcher并调用watcher上的update方法,执行初始化视图阶段绑定的更新函数调用updater完成视图更新。

双向数据绑定的实现

主要关注v-model这个指令,模板编译时在dom上绑定input事件监听,事件被触发时(如输入框数据变化),则访问observer劫持的对应属性的set()访问器,继而触发视图更新

    // 双向数据绑定的处理:
    // 该指令的设计类似于 事件指令+普通指令
    // 相对于普通指令,它额外绑定了input事件监听,当input事件触发,调用被劫持属性的set()
    model: function (node, vm, exp) {
        this.bind(node, vm, exp, 'model');

        var me = this,
            val = this._getVMVal(vm, exp);
        node.addEventListener('input', function (e) {
            var newValue = e.target.value;
            if (val === newValue) {
                return;
            }

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