Vue

Vue - MVVM 原理

Posted by huangqing on May 7, 2020

MVVM

MVVM双向数据绑定是通过数据劫持+发布订阅模式Object.defineProperty

let obj={};
let theValue;

Object.defineProperty(obj,'theKeyName',{
    //value:'theValue',
    configurable:true,  //可以配置对象,删除属性
    //writable:true,    //可以修改对象
    enumerable:true,    //可以枚举:默认情况下通过defineProperty定义的属性是不能被枚举(遍历)的
    get(){              //get,set设置与writable,value互斥
        return value;
    },
    set(value){
        theValue=value;
    }
});

参考Vue

<div id="app">
    <h1></h1>
</div>
let mvvm = new Mvvm({
    el:'#app',
    data:{
        title:'Mvvm'
    }
});

打造MVVM

function Mvvm(options = {}){
    this.$options = options;
    let data = this._data = this.$options.data;

    _observe(data);
}

数据劫持

  • 观察对象,给对象增加Object.defineProperty
  • vue特点是不能新增不存在的属性 不存在的属性没有getset
  • 深度响应 每次赋予一个新的对象时会给这个新对象增加defineProperty
function Observe(data){
    // 据劫持就是给对象增加get,set
    // 把data属性通过defineProperty的方式定义属性
    for(let key in data){
        let val = data[key];
        // 递归继续向下找,实现深度的数据劫持
        _observe(val);
        
        Object.defineProperty(data,key,{
            configurable:true,
            get(){
                return val;
            },
            set(value){
                if(val===value){
                    return;
                }
                val = value;
                // 当设置为新值后,也需要把新值再去定义成属性
                _observe(value);
            }
        })
    }
}

function _observe(data){
    if(!data || typeof data !== 'object'){
        return;
    }

    return new Observe(data);
}

数据代理

数据代理就是让我们每次拿data里的数据时,不用每次都写一长串,如mvvm._data.a.b这种,我们其实可以直接写成mvvm.a.b这种显而易见的方式

function Mvvm(options = {}){
    _observe(data);
    // this 代理了this._data
    for(let key in data){
        Object.defineProperty(this,key,{
            configurable:true,
            get(){
                return this._data[key];
            },
            set(value){
                this._data[key]=value;
            }
        });
    }
}

数据编译

function Mvvm(options = {}){
    ...

    new Compile(options.el,this);
}
function Compile(el, vm) {
    // 将el挂载到实例上方便调用
    vm.$el = document.querySelector(el);
    // 在el范围里将内容都拿到,当然不能一个一个的拿
    // 可以选择移到内存中去然后放入文档碎片中,节省开销
    let fragment = document.createDocumentFragment();

    while (child = vm.$el.firstChild) {
        fragment.appendChild(child);    // 此时将el中的内容放入内存中
    }
    // 对el里面的内容进行替换
    function replace(frag) {
        Array.from(frag.childNodes).forEach(node => {
            let txt = node.textContent;
            let reg = /\{\{(.*?)\}\}/g;   // 正则匹配{}

            if (node.nodeType === 3 && reg.test(txt)) {
                function replaceTxt() {
                    node.textContent = txt.replace(reg, (matched, placeholder) => {   
                        console.log(placeholder);   // 匹配到的分组 如:song, album.name, singer...
                        new Watcher(vm, placeholder, replaceTxt);   // 监听变化,进行匹配替换内容

                        return placeholder.split('.').reduce((val, key) => {
                            return val[key]; 
                        }, vm);
                    });
                };
                replaceTxt();
            }
        });
    }

    replace(fragment);  // 替换内容

    vm.$el.appendChild(fragment);   // 再将文档碎片放入el中
}

发布订阅

发布订阅主要靠的就是数组关系,订阅就是放入函数,发布就是让数组里的函数执行

// 发布订阅模式
function Dep(){
    // 一个数组(存放函数的事件池)
    this.subs = [];
}

Dep.prototype = {
    addSub(sub){
        this.subs.push(sub);
    },
    notify(){
        // 绑定的方法,都有一个update方法
        this.subs.forEach(sub=>sub.update());
    }
}

// 监听函数
// 通过Watcher这个类创建的实例,都拥有update方法
function Watcher(fn){
    this.fn = fn;
}

Watcher.prototype.update = function(){
    this.fn();
}

let watcher= new Watcher(()=> console.log(1));
let dep = new Dep();

dep.addSub(watcher);
dep.addSub(watcher);

dep.notify(); // 1 , 1

数据更新视图

  • 现在我们要订阅一个事件,当数据改变需要重新刷新视图,这就需要在replace替换的逻辑里来处理
  • 通过new Watcher把数据订阅一下,数据一变就执行改变内容的操作

1 Compile

function Compile(el, vm) {
    ...
    // 替换的逻辑
    //node.textContent=txt.replace(reg,val).trim();

    // 给Watcher再添加两个参数,用来取新的值(newVal)给回调函数传参
    new Watcher(vm,RegExp.$1,newVal => {
        node.textContent = txt.replace(reg,newVal).trim();
    });
}

RegExp.$1RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串

2 Watcher

function Watcher(vm,exp,fn){
    this.fn = fn;
    this.vm = vm;
    this.exp = exp;

    Dep.target = this;

    let arr = exp.split('.');
    let val = vm;
    arr.forEach(key=>{
        // 获取到this.a.b,默认就会调用get方法
        val = val[key];
    });

    Dep.target = null;
}

// set 修改值时执行 dep.notify,其内部执行的是 watcher.update
Watcher.prototype.update = function() {
    // notify的时候值已经更改了
    // 再通过vm, exp来获取新的值
    let arr = this.exp.split('.');
    let val = this.vm;
    arr.forEach(key => {
        val = val[key];
    });

    this.fn(val);
}

3 Observe

function Observe(data){
    let dep = new Dep();

    ...

    Object.defineProperty(data,key,{
        get(){
            // 将watcher添加到订阅事件中 [watcher]
            Dep.target&&dep.addSub(Dep.target);
            return val;
        },
        set(newVal){
            if(val === newVal){
                return;
            }
            val = newVal;
            _observe(newVal);
            // 执行所有watcher的update方法
            dep.notify();
        }
    });
}

双向数据绑定

function replace(frag){
    ...

    if(node.nodeType === 1){
        let nodeAttr = node.attributes;
        Array.form(nodeAttr).forEach(attr =>{
            let name = attr.name; //v-model type
            let exp = attr.value;
            if(name.includes('v-')){
                node.value = vm[exp];
            }

            new Watcher(vm,exp,function(newVal){
                // 当watcher触发时会自动将内容放进输入框中
                node.value = newVal;
            });

            node.addEventListener('input',e=>{
                let newVal=e.target.value;
                // 相当于给this.c赋了一个新值
                // 而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新
                vm[exp] = newVal;
            });
        })
    }

    if(node.childNodes && node.childNodes.length){
        replace(node);
    }
}

总结

通过Object.definePropertygetset进行数据劫持

通过遍历data数据进行数据代理到this

通过{{}}对数据进行编译

通过发布订阅模式实现数据与视图同步

Only 10 分钟,给你圈出 MVVM 原理重难点