MVVM

257 阅读2分钟

理解

M: 简单理解为script里面的data数据 里面数据的值是看不到的 从后端传来的数据

V:在el管理的模块为view视图 在浏览器页面可以看到的数据

VM:数据监听与绑定

在视图中可以双向绑定模型中的值 改变视图中的值 则模型中的值相应的改变 相反也是

原理

mvvm分为两个模块 一个时编译模板Complier 找到页面中需要替换的指令并对其数据进行替换 一个是Observer 把数据变成响应式数据

下面详细介绍

分解 Vue 实例

如何入手?首先从怎么使用Vue开始。让我们一步步解析Vue的使用:

let vm = new Vue({
    el: '#app'
    data:{
        school:{
            name:'beida',
            age:100
        }
    },
})

vue

当new Vue({}) 时就知道需要配置一个Vue类 {}里面的会传入到Vue类的constructor

class Vue{
    constructor(options){ //options传来的是一个对象
        this.$el = options.el
        this.$data = options.data
    }
    if(this.$el){
        //把数据变成响应式数据
        new Observer(this.$data)
        //编译模板
        new Complier(this.$el,this)
    }
    
}

Complier

在.html中el管理的模块中使用v-model v-text v-html v-if v-show {{}}等时数据并不会显示在浏览器页面上

在Vue类中new Complier() 则说明需要一个Complier类 传入到这个类的constructor

class Complier{
    constructor(el,vm){
        this.el = isElementNode(el) ? el : document.queryselector(el)
        this.vm = vm
    }
    //会把浏览器页面的所有节点放到自己建好的空间中 这时浏览器页面上没有任何东西
    let fragment = node2fragment(this.el)
    
    //替换数据
    this.complier(fragment)
    
    //把替换好的数据重新放到浏览器上
    this.el.appendChild(fragment)
    
}

判断是元素节点还是其他节点(属性节点、文本节点)

isElementNode(node){
//1代表元素节点 2代表属性节点 3代表文本节点
    return node.nodeType == 1
}

把节点放到自己建好的空间

node2fragment(node){
    let fragment = document.createDocumentFragment()
    let firstChild
    //因为不知道有多少个节点 所以使用while循环
    while(firstChild = node.firstChild){
        fragment.appendChild(firstChild)
    }
    return fragment
}

替换数据

首先需要判断是元素节点 还是文本节点 然后对其分别做处理

complier(node){
    // console.dir(node) //#document-fragment 有时会把标签显示出来
    //childNodes不包括li 那是孙子节点了
    // console.dir(node.childNodes) //[text, input, text, div, text, div, text, ul, text] 换行是一个文本节点 如果都放到一行的话 就不会出现文本节点
    // console.log(Array.isArray(node.childNodes))  //false 伪数组
    let childNodes = node.childNodes;
    // console.log(Array.isArray([...childNodes])); //true
    // ...前面的一句必须加上分号
    [...childNodes].forEach(child => {
        if(this.isElementNode(child)){
            //是元素节点
            // console.log(child+'是一个元素节点')
            this.complier(child)
            this.complierElement(child)
        }else{
            //是文本节点
            // console.log(child+'是一个文本节点')
            this.complierText(child)
        }
    });
}

对元素节点进行处理之找到是以'v-'开头的属性 并且调用CompilerUtil

complierElement(node){
    // console.dir(node) //所有的元素节点(例如<input type="text" v-model='school.name'>)
    let attributes = node.attributes;   
    // console.log(typeof attributes) //object 伪数组
    [...attributes].forEach(attr=>{
        // console.log(attr) //type='text' v-model='school.name'
        // console.log(typeof attr) //object
        let {name,value:expr} = attr
        // console.log(name) //必须是name type v-model
        // console.log(value) //text school.name
        if(this.isDirective(name)){
            //元素节点的属性节点名字是以'v-'开头
            // console.log(name+'是一个vue指令') //v-model是一个指令
            // console.log(name.split('-')) //['v','model']
            let [,directive] = name.split("-");
            // console.log(directive) //model
            CompilerUtil[directive](node,expr,this.vm);
            
        }
    }) 
}

对元素节点进行处理之获取到data里面的数据并进行更新数据 写一个对象 里面包含了不同指令的处理方法 是单独放出来的 没有在任何的类中

CompilerUtil = {
    getVal(vm,expr){
        return expr.split(".").reduce((data,current)=>{
            // console.log(expr.split('.')) ['school','name']
            //一共输出两次 
            // console.log(data)  //1 vue实例 2{name:'beida',age:100}
            // console.log(current) //1school 2name
            return data[current]
        },vm.$data);
    },
    setVal(vm,expr,value){
        // console.log(vm)  //vue实例
        // console.log(expr) //school.name
        // console.log(value) //在input框中重新输入的值
        // console.log(expr.split('.')) //['school','name']
        expr.split('.').reduce((data,current,index,arr)=>{
            //一共输出两次
            // console.log(data) //1vue实例 2{name:'beida',age:100} 
            // console.log(current) //1school 2name
            // console.log(index) //1 0 2 1
            // console.log(arr) // 1['school','name'] 2['school','name']
            if(index == 1){
                // console.log(data[current]) //beida
                return data[current] = value
            }
            return data[current]
        },vm.$data)
     },
    model(node,expr,vm){ 
        let fn =  this.updater["modelUpdater"]
        // 给输入框添加一个观察者,如果后面data数据改变了,则也需要改变视图上的数据
        new Watcher(vm,expr,(newVal)=>{
            //更新数据
            fn(node,newVal)
        })
        //给input输入框添加一个监听事件 这样会监听视图数据改变 那么data数据也改变
        node.addEventListener('input',(e)=>{
            // console.log(e.target.value)
            let value = e.target.value
            this.setVal(vm,expr,value)
        })
        let value = this.getVal(vm,expr)
        fn(node,value);
    },
}

对文本节点进行处理之找到{{}}并调用ComplierUtil['text']

  complierText(node){
    // console.dir(node) //#text  '{{school.name}}' '{{school.age}}' '1' '1' 
    let content = node.textContent;
    // console.log(content) //得到所有的文本节点中的内容 {{school.name}} {{school.age}} 1 1
    let reg = /\{\{(.+?)\}\}/;  // {}在正则中有特殊的含意,需要转义
    if(reg.test(content)){
        // console.log(content) // {{school.name}}  {{school.age}}
        CompilerUtil['text'](node,content,this.vm)
    }
}

对文本节点进行处理之{{}}用什么代替和找到data里面的数据并更新 在Complier

ComplierUtil = {
    //更新数据
    updater:{
        textUpdater(node,value){
        // textContent得到文本节点中内容
        node.textContent = value
        }
    },
    // 得到新的内容
    getContentValue(vm,expr){
        return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            return this.getVal(vm,args[1])
        })
    },
    
    text(node,expr,vm){
        let fn =  this.updater["textUpdater"]
        //{{}}用什么代替
        let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            // console.log(args)
            //["{{school.name}}", "school.name", 0, "{{school.name}}"]
            //["{{school.age}}", "school.age", 0, "{{school.age}}"]
            //["{{getSchool}}", "getSchool", 0, "{{getSchool}}"]
            //添加一个water 如果后面data数据改变了,则也需要改变视图上的数据
            new Watcher(vm,args[1],()=>{
                fn(node,this.getContentValue(vm,expr));
            })
            return this.getVal(vm,args[1])
        })
    fn(node,content);
},
}

Observer

实现把数据变成响应式数据 获取数据或者数据改变时都会触发 这样vm就可以通知观察者进行改变数据 当获取数据时把所有的观察者存储到Dep中 数据改变时它会触发dep.notify()=>通知观察者改变数据water.update()

在Vue类中new Observer(this.$data) 说明要写一个Observer类

class Observer{
    constructor(data){
        // console.log(data) //不实现observer这个方法时  数据不是响应式的
        this.observer(data)
    }
    observer(data){ 
        if(data && typeof data == 'object'){
            for(let key in data){
                this.defineReactive(data,key,data[key])
            }
        }
    }
    defineReactive(obj,key,value){
        this.observer(value);  
        // 如果一个数据是一个对象,也需要把这个对象中的数据变成响应式
        let dep = new Dep(); // 不同的watcher放到不同的dep中
        Object.defineProperty(obj,key,{
            // 当你获取school时,会调用get
            get(){
                Dep.target && dep.subs.push(Dep.target)
                // console.log("get....")
                // console.log(dep.subs)
                return value
            },
            // 当你设置school时,会调用set
            set:(newVal)=>{
                // 当赋的值和老值一样,就不重新赋值
                if(newVal != value){
                    this.observer(newVal)
                    value = newVal
                    // console.log('set...')
                    dep.notify();
                }
            }
        })
    }
}

Dep

为了存储不同的观察者 在Observer中new Dep() 所以需要建一个Dep类

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

Water

在元素节点或者文本节点的值改变时需要new Water 把新值重新渲染

在new Water()时它会监测到哪个属性改变 并把旧的值存储 如果改变 会把调自己的回调函数cb 把新值传过去

class Watcher{
constructor(vm,expr,cb){
    this.vm = vm
    this.expr = expr
    this.cb = cb
    this.oldValue = this.get()
    // console.log(this.oldValue)
}
get(){
    Dep.target = this
    let value = CompilerUtil.getVal(this.vm,this.expr)
    Dep.target = null
    return value
}
update(){
    let newVal = CompilerUtil.getVal(this.vm,this.expr)
    // console.log(newVal)
    if(newVal != this.oldValue){
        this.cb(newVal)
    }
}
}

使用vue脚手架时 在console直接输入vm.school 而不用输入vm.$data.school 这是因为把数据添加到了vue上面 在Vue类中

proxyVm(data){
    // console.log(data)  {school:{name:'beida',age:100}}
    for(let key in data){
        // console.log(key) //school
        // console.log(this)  //Vue {$el: "#app", $data: {…}} vue实例
        Object.defineProperty(this,key,{
            get(){
                return data[key]
            }
        })
    }
} 

然后在Vue类中添加

class Vue{
    this.proxyVm(this.$data)
}

在vue类中添加计算属性

class Vue{
    this.computed = options.computed
    for(let key in computed){
            // console.log(key) //getSchool
            Object.defineProperty(this.$data,key,{
                get:()=>{
                   return computed[key].call(this);
                }
            })
        }
}