Vue之实现MVVM

MVVM

MVVM是指Model-View-ViewModeljavascript

  • Model:模型
  • View: 视图,可组件化
  • ViewModel: 抽象视图,集成了数据绑定引擎,实现View和Model的双向绑定

Vue的运行机制

初始化流程

  1. 建立vue实例对象
  2. init过程当中初始化生命周期,初始化事件,初始化渲染,执行beforeCreate周期函数,初始化datapropscomputedwatcher,执行create周期函数
  3. 初始化后,调用$mount方法对vue实例进行挂载,包括模板编译,渲染,更新
  4. 若是定义了template,则须要进行编译:将template字符串编译为render function
  5. 调用$mountmountComponent方法,先执行beforeCreate周期函数,实例化一个渲染watcher,在它的回调函数(初始化及数据变化时执行)中调用updateComponent方法。
  6. 调用render方法将render function渲染成虚拟dom
  7. 生成虚拟DOM树后,调用update方法,update方法会调用pacth方法把虚拟DOM转换成真正的DOM节点

响应式流程

  1. init时会调用Object.defineProperty方法监听实例的数据变化(get和set方法),从而实现数据劫持。
  2. 在初始化的编译阶段,会读取vue实例中与视图相关的响应式数据,get函数会进行订阅收集(把监听watcher实例放到订阅者Dep的数组sub中),这是数据劫持和订阅发布模式就造成了ViewModel
  3. 数据或视图变化时,会触发数据劫持的set方法,set会通知Dep中相应的watcherwatcher调用update方法来更新视图。

MVVM实现思路

MVVM双向数据绑定原理是经过 数据劫持+发布订阅 实现的。html

经过Object.defineProperty()来给对象的属性添加get,set方法,在数据变更时触发相应的监听回调。vue

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。java

要实现一个MVVM的思路为:node

  • 实现一个数据劫持Observe,给对象添加get,set方法,监听到数据变更时,通知订阅者(Watcher)。get用来收集订阅,set用来派发更新。
  • 实现一个解析编译Compile,对每一个元素节点进行匹配,绑定和替换{{}}的内容
  • 实现一个监听Watcher,链接Observe和Compile,收到数据变更的通知,执行相应回调函数,更新视图
  • 实现一个消息订阅Dep,用一个数组来收集订阅者,数据变更触发notify,再调用订阅者的update方法
  • 实现一个Vue入口函数

具体实现

vue.js数组

//Vue构造函数
function Vue(option = {}) {
    this.$option = option;
    let data = this._data = this.$option.data;
    observe(data); // 数据劫持
    // 数据代理,简化data数据的写法,如vue._data.name变成vue.name
    for(let key in data) {
        Object.defineProperty(this, key, {
            configurable: true,
            get() {
                return this._data[key];
            },
            set(newVal) {
                this._data[key] = newVal;
            }
        })
    }
    //初始化computed,将this指向实例
    initComputed.call(this);
    // 数据编译,解析{{}}的内容
    new Compile(option.el, this);
    //执行mounted钩子函数
    option.mounted.call(this);
}

//数据劫持就是给对象增长get,set
function Observe(data) {
    let dep =new Dep();
    for(let key in data) {
        let val = data[key];
        observe(val) //递归继续向下,实现深度的数据劫持
        // Object.defineProperty定义对象的属性
        Object.defineProperty(data, key, {
            configurable: true, // 能够配置对象,删除属性
            get() {
                Dep.target && dep.addSub(Dep.target); //将watcher实例添加到订阅事件中
                return val
            },
            set(newVal){ //修改值的时候
                if(val == newVal) { //值相同就不理
                    return;
                }
                val = newVal; 
                observe(newVal); //把新值也定义成属性
                dep.notify(); //执行watcher中的update方法
            }
        })
    }
}

//递归函数
function observe(data) {
    if(!data || typeof data != 'object') return;
    return new Observe(data);
}

//编译函数
function Compile(el, vm){
    vm.$el = document.querySelector(el); // 将el挂载到实例上
    let fragment = document.createDocumentFragment(); // 建立一个新的空白文档片断
    while(child = vm.$el.firstChild) { //将el的内容都拿到,放入内存中,节省开销
        fragment.appendChild(child);        
    }   
    //替换内容 
    function replace(frag){
        Array.from(frag.childNodes).forEach(node => {
            let txt = node.textContent;
            let reg = /\{\{(.*?)\}\}/g; // 正则匹配{{}}
            if(node.nodeType === 1 && reg.test(txt)) { //既是文本节点又是大括号{{}}
               function replaceTxt() {
                   node.textContent = txt.replace(reg, (matched, placholder) => {
                       //placholder匹配到的分组,name,age
                       new Watcher(vm, placholder, replaceTxt); // 监听数据变化,替换{{}}的内容
                       return placholder.split('.').reduce((val, key) => { //reduce为数组的每一个元素依次执行回调函数
                           return val[key]; //将vm的数据传给val作初始值
                       }, vm)
                   })
               }
               replaceTxt();
            }

            //实现双向绑定
            if(node.nodeType === 1) {
                let nodeAttr = node.attributes; //获取元素上的属性,类数组
                Array.from(nodeAttr).forEach(attr => {
                    let name = attr.name; // v-model type
                    let exp = attr.value; // c
                    if(name.includes('v-')){
                        node.value = vm[exp]; // 将vm中的c的值,挂载到节点上
                    }
                    //监听数据变化
                    new Watcher(vm, exp, function(newVal){
                        node.value = newVal;
                    })
                    node.addEventListener('input', e => {
                        let newVal = e.target.value;
                        //给vm中的值赋值
                        vm[exp] = newVal;
                    })
                })
            }
            //子节点
            if(node.childNodes && node.childNodes.length) {
                replace(node);
            }
        })
    }
    replace(fragment);
    vm.$el.appendChild(fragment);
}

//发布订阅,把函数放入数组就是订阅,发布就是让函数执行
function Dep(){
    this.subs = [];
}
Dep.prototype.addSub = function(sub) {
    this.subs.push(sub);
}
Dep.prototype.notify = function() {
    this.subs.forEach(sub => sub.update());
}

//监听函数,给这个类建立的实例,添加update方法
function Watcher(vm, exp, fn){
    this.fn = fn; //将fn放到实例上
    this.vm = vm;
    this.exp = exp;
    // 定义一个属性,target是Dep的一个静态属性,是一个全局watcher,dep其实是对watcher的一种管理
    Dep.target = this;
    let arr = exp.split('.');
    let val = vm;
    arr.forEach(key => {
        val = val[key]; //获取值的时候调用get()方法
    })
    Dep.target = null;
}
Watcher.prototype.update = function() {
    // 值已经修改,再经过vm,exp来获取新的值
    let arr = this.exp.split('.');
    let val = this.vm;
    arr.forEach(key => {
        val = val[key]; //经过get()获取到新的值
    })
    this.fn(val); //fn为替换{{}}的内容
    
}

//实现Computed
function initComputed() {
    let vm = this;
    let computed = this.$option.computed; // 从option上拿到computed属性
    Object.keys(computed).forEach(key => {
        Object.defineProperty(vm, key, {
            // 判断computed的key是对象仍是函数,若是是函数会调get方法,若是是对象,手动调get方法
            // sum获取a,b的值会调get方法
            get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
            set() {}
        })
    })
}
复制代码

测试:app

<div id="mvvm">
  <p>{{name}}</p>
  <p>{{age}}</p>
  <input type="text" v-model='c'/>
  <p>{{c}}</p>
</div>

<script> let mvvm = new Vue({ el:'#mvvm', data: { name: '小明', age: 20, a: 10, b: 30, c: '' }, computed: { sum() { return this.a + this.b }, noop() {} }, mounted() { setTimeout(() => { console.log('完成'); }, 1000); } }) </script>
复制代码

参考:dom

juejin.im/post/5abdd6…mvvm

juejin.im/post/5cd8a7…函数

相关文章
相关标签/搜索