1120-MVVM框架是如何实现双向数据绑定剖析。

239 阅读5分钟

MVVM框架,实现双向数据绑定。

核心:编译compile、数据劫持observer、观察者watcher 观察者模式

思路:

1.model影响视图:编译时注册watcher,在注册watcher,调用get,通过observer数据劫持get方法,将多个观察者统一管理起来。当改变数据时,调用set方法,将收拢的对应观察者的upadte方法更新。
2.视图影响model:编译时注册wather,node节点绑定对应的事件,事件触发时,更改数据模型。

js模拟实现MVVM

MVVM.js,集中统一;供外调用;
class MVVM{
    constructor(options){
        this.$data = options.data;
        this.$el = options.el;
        if(this.$el){
            //数据劫持
            new Observer(this.$data);
            //代理data
            this.proxyData(this.$data);
            //用数据和元素进行编译。
            new Compile(this.$el,this);
        }
    }
    //将data的属性挂载到实例上,这样就可以取this.***了l
    proxyData(data){
        Object.keys(data).forEach(key=>{
            Object.defineProperty(this,key,{
                get(){
                    return data[key];
                },
                set(newVal){
                    data[key] = newValue;        
                }
            })
        })
    }
}
Observe.js,数据劫持;收拢watcher统一管理;通知watcher更新;
class Observer(){
    constructor(data){
        this.observe(data);
    }
    //数据劫持(这里我们只处理对象{}的监听)
    observe(data){
        if(Object.prototype.toString.call(data) !='[object Array]'){
            //如果是数据,会有另外的方法进行监听数组变化。
            //vue是通过伪改数组原生方法,做到监听的,比如push pop reverse等
        }
        if(!data || Object.prototype.toString.call(data) !='[object Object]'){
            return;
        };
        Object.keys(data).forEach(key=>{
            //劫持当前key
            this.defineReactive(data,key,data[key]);
            //深度劫持
            this.observe(data[key]);
        })
    }
    //数据响应式 defineProperty
    defineReactive(obj,key,value){
        let that = this;
        //每个数据都有一个dep用来收拢存放watcher。当数据改变时,通知所有watcher更新;
        let dep = new Dep();
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get(){
                //*收拢watcher;编译过程中每匹配到表达式或者指令时,就创建一个watcher。创建watcher时,调用get,通过Dep.target将watcher传过来,由dep进行收拢。
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set(newValue){
                //数据发生改变
                if(newValue!=value){
                    value = newValue;
                    //重新定义劫持,this不是当前Observer实例,因为在set时,是vm.key=***,所以this不是当前Observer实例
                    that.observe(newvalue)
                    //通知watcher更新
                    dep.notify();
                }
            }
        })
    }
}
//观察者模式,统一对watcher进行管理
class Dep(){
    constructor(){
        //存放watcher的数组
        this.subs = []
    }
    //添加watcher
    addSub(watcher){
        this.subs.push(watcher)
    }
    //通知所有watcher更新
    notify(){
        this.subs.forEach(watcher=>{
            watcher.update();
        })
    }
}

Compile.js,元素和数据进行编译,首次渲染视图;创建watcher,定义数据变化后如何更改视图;绑定事件,定义视图变化如何更改model
class Compile{
    constructor(el,vm){
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        //挂载的dom节点存在
        if(this.el){
            //1.先把这些真是的DOM移动到内存中,真实DOM就不存在了。降低性能,减少回流;
            let fragment =this.node2Fragment(this.el)
            //2.编译:提取想要的元素节点v-model和文本节点{{}},替换成model对应的数据;创建watcher,定义数据更新如何更新;绑定事件,定义视图变化如何更改model;
            this.complile();
            //3.把编译好的fragment放回页面中
            this.el.appendChild(fragment)
            
        }
    }
    /*辅助方法*/
    //验证是不是node节点
    isElementNode(){
        return node.nodeType == 1;
    }
    //验证是不是指令
    idDirective(name){
        return name.includes('v-')
    }
    
    
    /*核心方法*/
    //将真实dom转移到内存中,fragment,真实DOM就不存在了;
    node2Fragment(el){
        let fragment = document.createDocumentFragment();
        let firstChild = el.firstChild;
        while(firstChild){
            fragment.appenChild(firstChild);
            firstChild = el.firstChild;
        }
        return fragment;
    }
    //递归编译
    compile(fragment){
        //childNodes获取子元素,只能获取到第一层子元素
        let childrens = fragment.childNodes;
        Array.from(childrens).forEach(node=>{
            if(this.isElementNode(node)){
                //编译当前这个元素节点
                this.compileElementNode(node);
                //继续深入检查
                this.compile(node)
            }else{
                //编译文本节点
                this.compileText(node)
            }    
        })
    }
    //编译元素节点
    compileElement(node){
        let attrs = node.attributes;//取出所有节点属性 {0:{name:'',value:''},1:{name:'',value:''}}
        Array.from(attrs).forEach(attr=>{
            let attrName = attr.name; //例如:v-model v-text...
            //判断是不是指令
            if(this.isDirective(attrName)){
                let expr = attr.value; //例如:message.name 
                let [,type] = attrName.split('-');// type=> 例如model
                CompileUtil[type](node,this.vm,expr)
            }
        })
    }
    //编译文本节点
    compileText(node){
        let expr = node.textContent;// 例如:{{a}}{{b}} ;  {{message.name}} 
        let reg = /\{\{\([^}]+)}\}/g; 
        //判断是不是 包含{{}}表达式
        if(reg.test(expr)){
           CompileUtil['text'](node,this.vm,expr)
        }
    }
    
}

CompileUtil = {
    //获取实例上对应的数据
    getVal(vm,expr){
        //例如:message.name
        expr = expr.split('.');
        return expr.reduce((prev,next)=>{
            return prev[next]
        },vm.$data)
    }
    //文本节点的内容进行编译
    getTextVal(vm,expr){
        //例如: expr 考虑{{a}}{{b}};{{message.name}} 多种文本方式
        let reg = /\{\{\([^}]+)}\}/g; 
        return   expr.replace(reg,(...args)=>{
            retrun this.getVal(vm,args[1])
        })
    }
    //修改实例上的数据
    setVal(vm,expr,value){
        expr = expr.split('.');
        return expr.reduce((prev,next,index)=>{
            if(index == expr.length-1){
                prev[next] = value;
            }
            return prev[next]
        },vm.$data)
    }
    //输入框处理 v-mode
    model(node,vm,expr){
        //首次渲染
        let updateFn = this.updater['modelUpdater'];
        updateFn && updateFn(node,this.getVal(vm,expr));
        //创建watcher,数据变化影响视图
        new Watcher(vm,expr,(newValue)=>{
            updateFn && updateFn(node,newValue)
        })
        //绑定事件,视图变化 影响数据
        node.addEventListener('input',(e)=>{
            let newValue = e.target.value;
            this.setVal(vm,expr,newValue)
        })
        
    }
    //文本处理 
    //expr=> 例如{{a}}{{b}} ;  {{message.name}} 考虑多种文本方式
    text(node,vm,expr){
        //首次渲染
        let updateFn = this.updater['textUpdater'];
        let value = this.getTextVal(vm,expr);
        updateFn && updateFn(node,value);
        //创建watcher
        let reg = /\{\{\([^}]+)}\}/g; 
        expr.replace(reg,(...args)=>{
            new Watcher(vm,args[1],(newValue)=>{
                //不能用newValue,newValue只是其中一个表达式的值。
                //如果是{{a}}{{b}}这种文本节点,其中一个改变就需要将该文本节点渲染一次,就需要重新根据expr {{a}}{{b}}去重新渲染整个文本节点,所以用的是this.getTextVal(vm,expr),而不是newValue;
                 updateFn && updateFn(node,this.getTextVal(vm,expr));
            })
        })
    }
    updater:{
        //文本更新
        textUpdater(node,value){
            node.textContent = value
        }
        //输入框更新
        modelUpdater(node,value){
            node.value = value;
        }
    }
}
Watcher.js ,创建观察者,编译过程中每遇到一个表达式或者v-model指令,就创建一个观察者。 每一个数据 对应多个观察者
calss Watcher{
    constructor(vm,expr,cb){
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        //先保存当前的值
        this.value = this.get();
    }
    getVal(vm,expr){
        expr = expr.split('.');//message.name
        return expr.reduce((prev,next) =>{
            return prev[next]
        },vm.$data)
    }
    get(){
        //在获取值的时候调用observer get方法,通过Dep.target 传递watcher,将watcher统一收拢管理到对应的subs中
        Dep.targer = this;
        let value = this.getVal(this.vm,this.expr)
        Dep.target = null;
        return value;
    }
    //对外报漏的方法,当数据发生改变在observer set方法中,会调用sub的notify方法,nofity方法将所有watcher执行update方法
    update(){
        let newValue = this.getVal(this.vm,this.expr);
        let oldValue = this.value;
        if(newValue != oldValue){
            this.cb(newValue);//
        }
    }
}