MVVM理解

373 阅读3分钟

一、MVVM应用

 MVVM最最根本的就是实现数据改变,视图更新,视图变化,数据也发生改变。

二、MVVM原理

一、Vue整体调度与编译模板Compiler

  第一步:在创建Vue实例时直接new上了Vue,且在html文件中是直接script标签引入,不再使用import,所以要在js中创建一个Vue类,也不需要export将类导出啦!Vue类中constructor的参数options是创建的Vue实例中传递过去的对象,里面存在着$el参数。

  第二步:如果对象中el存在,就能找到上面的HTML模板(视图),将模板中需要替换数据的元素进行替换,又称为编译(编译模板)。当存在el会调用编译模板(将$el和vue实例传过去,这样就找到模板)。
第三步:创建编译模板这样一个类,编译之前判断要编译的是否为元素节点; 判断el是否为元素节点,是的话就使用它本身,否则就拿到#app对应的一堆元素节点。
  第四步:HTML要渲染出一张网页,要形成一棵DOM树,在DOM树上有两类节点:元素节点和文本节点。根据节点的属性nodeType来判断是哪种节点:元素节点:nodeType属性返回1;属性节点:nodeType属性返回2。
第五步:拿到模板后将模板中的内容一条条挪到内存空间(文档碎片)中,主要利用到了appendChild,往文档碎片当中移动性的扔数据。
第六步:将文档碎片中的数据进行替换(模板编译),将编译后的数据返回到网页中渲染出来。


先拿到节点,判断是元素节点还是文本节点   进行模板编译具体操作:
1)首先拿到属性节点,判断属性节点是否是以v-打头的指令,通过正则也找到插值表达式对应的节点;

2)封装个对象(工具),里面存在着不同指令对应的不同处理方法;

    // 封装方法,存放不同指令对应的不同处理方法
    CompilerUtil = {
        // 处理得到指令对应的值 
        getval(vm, expr) {
            return expr.split(".").reduce((data, current) => {
                return data[current]
            }, vm.$data)
        },
        // 处理v-model指令,实现数据绑定
        model(node, expr, vm) {
            let val = this.getval(vm, expr)
            let fn = this.updater["modelUpdater"]
            fn(node, val)
        },
        // 插值表达式去掉  v-text
        text(node, content, vm) {
            // 通过正则使数据中{{}}全部由后面得到的数据替换
            let newcontent = content.replace(/\{\{(.+?)\}\}/g, (...args) => {
                return this.getval(vm, args[1])
            })
            let fn = this.updater["textUpdater"]
            fn(node, newcontent)
        },
        // 更新数据
        updater: {
            // 更新v-model指令数据
            modelUpdater(node, value) {
                node.value = value;
            },
            // 更新文本内容数据
            textUpdater(node, content) {
                node.textContent = content;
            }
        }
    }

  3)编译元素节点:封装v-model对应的处理方法,给input框上附上value值,将原本的死数据给替换掉。

  4)编译文本节点:封装插值表达式对应的处理方法,使用正则将{{}}中的内容替换掉。

  5)实现真正的文本节点与元素节点的编译

二、Observer数据劫持

  Observer实现数据劫持,把数据变成响应式(给数据添加get()和set()),当数据修改时可以感应到数据修改。

  响应式数据设置时可以利用方法defineProperty来精细化设置某个属性,给其添加上get和set方法。

三、Watch观察者

  在编译元素、文本时,触发相应方法来修改数据,此时会创建watcher(),进入观察者模式。观察者中保存老状态,数据改变更新状态,调用回调函数,根据状态干件事。

四、Dep类

  Dep类是存储观察者的;当获取数据时将watcher推到Dep中,数据改变时会通知watcher调用update方法。

五、完整代码

class Dep{
  constructor(){
      this.subs = [];
  }
  addSub(watcher){
     this.subs.push(watcher)
  }
 notify(){
     this.subs.forEach(watcher=>watcher.update())
 }
}

class Watcher{
  constructor(vm,expr,cb){
     this.vm = vm;
     this.expr = expr;
     this.cb = cb;
     this.oldState = this.get();
  }
  get(){
      Dep.target = this
      let state = CompilerUtil.getval(this.vm,this.expr)
      Dep.target = null;
      return state;
  }
  update(){
    let newstate = CompilerUtil.getval(this.vm,this.expr)
    if(newstate !=this.oldState){
        this.cb(newstate)
    }
  }
}

class Observer{
    constructor(data){
     this.observer(data)
    }
    observer(data){
       if(data && typeof data=='object'){
          for(let key in data){
              this.defindReactive(data,key,data[key])
          }
       }
    }
    defindReactive(obj,key,value){
        this.observer(value)
        let dep = new Dep()
        Object.defineProperty(obj,key,{
            get(){
                Dep.target && dep.subs.push(Dep.target)
                return value
            },
            // 设置数据
            set:(newval)=>{
                if(newval != value){
                    this.observer(newval)
                    value = newval;
                    dep.notify()//通知watcher调用update方法
                }
            },
        })
    }
}

class Compiler{
    constructor(el,vm){
      this.el =  this.isElementNode(el) ? el : document.querySelector(el)
      this.vm = vm;
      let fragment = this.node2fragment(this.el)
      this.compile(fragment); 
      this.el.appendChild(fragment)
    }
    compileElement(node){
     let attributes = node.attributes;
      [...attributes].forEach(attr=>{
       let {name,value:expr} = attr
       if(this.isDirective(name)){
        let [,directive] = name.split("-")
        CompilerUtil[directive](node,expr,this.vm)
       }
      })
    }
    isDirective(attrName){
      return attrName.startsWith("v-")
    }
    compileText(node){
     let content = node.textContent
     let reg = /\{\{(.+?)\}\}/; 
     if(reg.test(content)){
        CompilerUtil['text'](node,content,this.vm)
     }
    }
    compile(fragment){
        let childNodes = fragment.childNodes;
        [...childNodes].forEach(child=>{
           if(this.isElementNode(child)){
            this.compileElement(child)  
            this.compile(child)
           }else{
            this.compileText(child)
           }
        })
    }

    node2fragment(node){
    let fragment = document.createDocumentFragment();
    let firstChild;
    while(firstChild=node.firstChild){
      fragment.appendChild(firstChild)
    }
    return fragment;
    }
    isElementNode(node){
        return  node.nodeType === 1;
    }
}

CompilerUtil = {
    getval(vm,expr){
    return expr.split(".").reduce((data,current)=>{
       return data[current]
    },vm.$data)
    },
    getContentValue(vm,expr){
        return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            return this.getval(vm,args[1])
        })
    },
    setValue(value,expr,vm){
        
        expr.split(".").reduce((data,current,index,arr)=>{
            if(index==arr.length-1){
               data[current] = value
            }
            return data[current]
        },vm.$data)
    },
    model(node,expr,vm){
       let val = this.getval(vm,expr)
       let fn = this.updater["modelUpdater"]
       new Watcher(vm,expr,(newstate)=>{
         fn(node,newstate)
       })
       node.addEventListener('input',(e)=>{
        let value = e.target.value
        this.setValue(value,expr,vm)
       })
       fn(node,val)
    },
    text(node,content,vm){
        let newcontent = content.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            new Watcher(vm,args[1],()=>{
                fn(node,this.getContentValue(vm,content))
            }) 
            return this.getval(vm,args[1])
        })
        let fn = this.updater["textUpdater"]
        fn(node,newcontent)
    },
    updater:{
        modelUpdater(node,value){
          node.value = value;
        },
        textUpdater(node,content){
            node.textContent = content;
        }
    }
}

class Vue{
    constructor(options){
        this.$el = options.el;
        this.$data = options.data;
        if(this.$el){
            new Observer(this.$data)
            // console.log(this.$data)
            // 需要让vm代理this.$data
            this.proxyVm(this.$data)
            new Compiler(this.$el,this)
        }
    }
    
    // 让vm代理data
    proxyVm(data){
    for(let key in data){
        // console.log(key)//school
        // console.log(data)//school对象
        // console.log(data[key])//school对象的值
        // 将数据key交给vm代理  给vm上挂上key这个属性
        Object.defineProperty(this,key,{
        //   vm.school  要想拿到数据就是获取数据,调用get()
        get(){
            return data[key]//将数据返回就OK
        }
        })
    }
 
    }
}
// vue中使用vue实例代理了data vm.$data.xxx--->vm.xxx

15,Observer实现数据劫持,把数据变成响应式(给数据添加get()和set()),当数据修改时可以感应到数据修改 16,在编译元素、文本时,触发相应方法来修改数据,此时会创建watcher(),进入观察者模式 17,观察者中保存老状态,数据改变更新状态,调用回调函数,根据状态干件事。 18,Dep类中包含多个Watcher。它是在获取数据时存放watcher的;当数据改变时会通知watcher调用update方法

1,Vue类是最高层,它负责整体的调度;当new上它是会调用constructor,且当el存在时会进行模板编译new Compiler; 2,Compiler类是编译模板;将元素节点转成文档碎片,在内存中编译元素,编译文本,将编译好的渲染到网页上 3,Observer类是数据劫持;把数据变成响应式(给数据添加get()和set()),当数据修改时可以感应到数据修改啦 4,Watcher类是观察者;在编译元素、文本时,触发相应方法来修改数据,此时会创建watcher(),调用watcher() 5,Dep类是存储观察者的;当获取数据时将watcher推到Dep中,数据改变时会通知watcher调用update方法 6,给input框监听一个input事件

编译模板:去页面中找到带有插值表达式和带有v-指令的节点 数据劫持:将所有数据都变成响应式的。 获取数据,触发get方法会创建watcher方法;将watcher全部存到Dep中 修改数据,触发set方法会调用notify方法通知Dep中所有Watcher调用其update方法 update方法会调用回调将数据重新赋值