vue中MVVM原理及其实现

一、了解 MVVM

  • MVVM - Model View ViewModel:数据,视图,视图模型。
  • 三者与 Vue 的对应:view 对应 template,vm 对应 new Vue({…}),model 对应 data。
  • 三者的关系:view 能够通过事件绑定的形式影响 model,model 能够通过数据绑定的模式影响到view,viewModel是把 model 和 view 连起来的连接器。

MVVM 框架的三大因素:

  • 响应式:Vue 如何监听到 data 的每个属性变动。

    数据劫持: 应用 Object.defineProperty(obj, 'property',{})来定义属性,将对象属性值的设置和拜访 (get,set) 都变成函数,别离在设置和获取该对象的该属性时调用执行。

  • 模板引擎:Vue 的模板如何被解析,指令如何解决
  • 渲染:Vue 的模板如何被渲染成 html,渲染过程是怎么的

二、实现办法

  1. 实现compile,进行模板的编译,包含编译元素(指令)、编译文本等,达到初始化视图的目标,并且还须要绑定好更新函数;
  2. 实现Observe,监听所有的数据,并对变动数据公布告诉;
  3. 实现watcher,作为一个中枢,接管到observe发来的告诉,并执行compile中相应的更新办法。
  4. 联合上述办法,向外裸露mvvm办法。

首先编辑一个html文件,如下:

<div id="app">    <input type="text" v-model="obj.name">    <div> {{obj.name}}</div>    {{message}}{{obj.name}}</div><script src="index.js"></script><script>    let vm = new MVVM({        el: '#app',//或document.querySelector('#app')        data: {            message: 'hello',            obj: {                name: 'susu'            }        }    });</script>

1.创立类MVVM

class MVVM {    constructor(options) {        this.$el = options.el;        this.$data = options.data;        if (this.$el) {            // 数据劫持 把对象所有的属性 减少get set 办法             new Observer(this.$data);            this.proxyData(this.$data);            // 用数据和元素进行编译            new Compile(this.$el, this);        }    }    // 把对象的属性全副绑定在实例上,this.xx    proxyData(data) {        // defineProperty解析 : https://www.jianshu.com/p/8fe1382ba135        Object.keys(data).forEach(key => {            Object.defineProperty(this, key, {                get() {                    return data[key];                },                set(newValue) {                    data[key] = newValue;                }            })        })    }}

2.实现compile(编译模板)

1.把实在DOM移入到内存中 fragment,因为fragment在内存中,操作比拟快

2.编译 : 提取想要的元素节点 v-model 和文本节点 {{}}

3.把编译好的fragment,增加到DOM中

class Compile {    constructor(el, vm) {        this.el = this.isElementNode(el) ? el : document.querySelector(el);        this.vm = vm;        if (this.el) {            // 1.把实在DOM移入到内存中 fragment,因为fragment在内存中,操作比拟快            let fragment = this.node2fragment(this.el);            // 2.编译 => 提取想要的元素节点 v-model 和文本节点 {{}}            this.compile(fragment);            // 3.把编译好的fragment,增加到DOM中            this.el.appendChild(fragment);        }    }    // 是否是元素节点    isElementNode(el) {        //nodeType : 1 Element 代表元素节点        return el.nodeType == 1;    }    node2fragment(el) {        // 在内存中,创立一个新的文档片段,        let fragment = document.createDocumentFragment();        let firstChild;        while (firstChild = el.firstChild) {            fragment.appendChild(firstChild);        }        // 返回虚构的节点对象,节点对象蕴含所有属性和办法。        return fragment;    }    // 编译    compile(fragment) {        let childNodes = fragment.childNodes;        Array.from(childNodes).forEach(node => {            if (this.isElementNode(node)) {                // 元素节点,编译元素                this.compileElement(node);                // 如果有子节点,再次执行                this.compile(node);            } else {                // 文本节点,编译文本                this.compileText(node);            }        })    }    // 编译文本 {{msg}}    compileText(node) {        let expr = node.textContent; //{{msg}}        let reg = /\{\{([^}]+)\}\}/g;        if (reg.test(expr)) {            CompileUtil.text(node, this.vm, expr);        }    }    // 编译元素 v-    compileElement(node) {        let attrs = node.attributes;        Array.from(attrs).forEach(attr => {            // 是否是指令            if (attr.name.includes('v-')) {                let expr = attr.value;// message                let type = attr.name.split('-')[1];//model                CompileUtil[type](node, this.vm, expr);            }        })    }}CompileUtil = {    // 文本处理    text(node, vm, expr) {        let updaterFn = this.updater.textUpdater;        let value = this.getTextVal(vm, expr);                // 监控数据变动 eg:因为{{message}} {{obj.name}} ,所以须要循环        expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {           new Watcher(vm,arguments[1],newValue => {                updaterFn && updaterFn(node, this.getTextVal(vm, expr));            })        })        updaterFn && updaterFn(node, value);    },    // 获取文本的key eg:message    getTextVal(vm, expr) {        return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {            return this.getValue(vm, arguments[1]);// message        })    },    // 获取data中对象的值    getValue(vm, expr) {        let arr = expr.split('.'); //解决对象obj.name =>[obj,name]        return arr.reduce((prev, next) => {            return prev[next]        }, vm.$data);    },    // 设置data中对象的值    setValue(vm, expr, value) {        let arr = expr.split('.');  //解决对象obj.name =>[obj,name]        arr.reduce((prev, next, curIndex) => {            if (curIndex == arr.length - 1) {                return prev[next] = value;            }            return prev[next];        }, vm.$data);    },    // 输入框解决    model(node, vm, expr) {        let updaterFn = this.updater.modalUpdater;        let value = this.getValue(vm, expr);        node.addEventListener('input', (event) => {            this.setValue(vm, expr, event.target.value);        })        // 监控数据变动        new Watcher(vm,expr,newValue => {            updaterFn && updaterFn(node, this.getValue(vm, expr));        })        updaterFn && updaterFn(node, value);    },    updater: {        textUpdater(node, value) {            node.textContent = value;        },        modalUpdater(node, value) {            node.value = value;        }    }}

3.实现observe(数据监听/劫持)

vue采纳的observe + sub/pub 实现数据的劫持,通过js原生的办法Object.defineProperty()来劫持各个属性的setter,getter,在属性对应数据扭转时,公布音讯给订阅者,而后触发相应的监听回调。

为何要监听 get,而不是间接监听 set ?

  • 因为 data 中有很多属性,有些被用到,有些可能不被用到
  • 只有被用到的才会走 get
  • 没有走到 get 中的属性,set 的时候咱们也无需关怀
  • 防止不必要的从新渲染

次要内容:observe的数据对象进行递归遍历,包含子属性对象的属性,都加上 setter和getter。

class Observer {    constructor(data) {        this.observe(data);    }    // 获取data的key value    observe(data) {        if (!data || typeof data != 'object') return;        Object.keys(data).forEach(key => {            this.defineReactive(data, key, data[key]);            this.observe(data[key]);        })    }    // 定义响应式    defineReactive(obj, key, value) {        let that = this;        // 每个变动的数据 都会对应一个数组,这个数组是寄存所有更新的操作        let dep = new Dep();        Object.defineProperty(obj, key, {            enumerable: true,            configurable: true,            get() {                console.log('observe',key,Dep.target);                Dep.target && dep.addSubs(Dep.target);                return value;            },            set(newValue) {                if (value != newValue) {                    // 如果是对象持续劫持                    that.observe(newValue);                    value = newValue;                    // 告诉更新                    dep.notify();                }            }        })    }}

实现数据劫持后,接下来的工作怎么告诉订阅者了,咱们须要在监听数据时实现一个音讯订阅器,具体的办法是:定义一个数组,用来寄存订阅者,数据变动告诉(notify)订阅者,再调用订阅者的update办法。

增加Dep类:

class Dep {    constructor() {        this.subs = [];    }    // 增加订阅    addSubs(watcher) {        this.subs.push(watcher);    }    // 告诉更新    notify() {        this.subs.forEach(watcher => {            watcher.update();        })    }}

4.实现watcher(订阅核心)

Observer和Compile之间通信的桥梁是Watcher订阅核心,其主要职责是:

1、在本身实例化时往属性订阅器(Dep)外面增加本人,与Observer建设连贯;

2、本身必须有一个update()办法,与Compile建设连贯;

3、当属性变动时,Observer中dep.notify()告诉,而后能调用本身(Watcher)的update()办法,并触发Compile中绑定的回调,实现更新。

// 观察者的目标就是给须要变动的那个元素减少一个观察者,当数据变动后执行对应的办法class Watcher {    constructor(vm, expr, cb) {        this.vm = vm;        this.expr = expr;        this.cb = cb;        // 获取老的值        this.value = this.get();    }    // 获取data中对象的值    getValue(vm, expr) {        let arr = expr.split('.');        return arr.reduce((prev, next) => {            return prev[next]        }, vm.$data);    }    get() {        Dep.target = this;        let value = this.getValue(this.vm, this.expr);        Dep.target = null;        return value;    }    // 更新,内部调用的办法    update() {        let newValue = this.getValue(this.vm, this.expr);        if (newValue != this.value) {            this.cb(newValue);        }    }}