仿vue 解读 MVVM思想

参考地址:

https://mp.weixin.qq.com/s/fRFnuBqdHn5kBgWiPQR7ew

什么是MVVM--MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。

在vue中,是通过Object.defineProperty(数据劫持)  +  观察者模式(发布+订阅)实现的数据之间双向绑定的

Object.defineProperty的用法

let obj = {};
let song = '发如雪'; 
obj.singer = '周杰伦';  

Object.defineProperty(obj, 'music', {
    // 1. value: '七里香',
    configurable: true,     // 2. 可以配置对象,删除属性
    // writable: true,         // 3. 可以修改对象
    enumerable: true,        // 4. 可以枚举
    // ☆ get,set设置时不能设置writable和value,它们代替了二者且是互斥的
    get() {     // 5. 获取obj.music的时候就会调用get方法
        return song;
    },
    set(val) {      // 6. 将修改的值重新赋给song
        song = val;   
    }
});

// 下面打印的部分分别是对应代码写入顺序执行
console.log(obj);   // {singer: '周杰伦', music: '七里香'}  // 1

delete obj.music;   // 如果想对obj里的属性进行删除,configurable要设为true  2
console.log(obj);   // 此时为  {singer: '周杰伦'}

obj.music = '听妈妈的话';   // 如果想对obj的属性进行修改,writable要设为true  3
console.log(obj);   // {singer: '周杰伦', music: "听妈妈的话"}

for (let key in obj) {    
    // 默认情况下通过defineProperty定义的属性是不能被枚举(遍历)的
    // 需要设置enumerable为true才可以
    // 不然你是拿不到music这个属性的,你只能拿到singer
    console.log(key);   // singer, music    4
}

console.log(obj.music);   // '发如雪'  5
obj.music = '夜曲';       // 调用set设置新的值
console.log(obj.music);   // '夜曲'    6

 仿VUE手写MVVM开始

先看个脑图:-- 代码的大致流程 (也可以不看,我其实就是瞎画的

 

图的摘抄地址:https://blog.csdn.net/feng_strong/article/details/74331823

先看下外部如何调用Mvvm类呢?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="school.name">
        <input type="text" v-model="a">
        {{a}}
        <div>
                {{school.aa}}{{school.bb}}
                <span>
                    {{school.bb}}
                </span>
        </div>
    </div>
    <script type="module">
        import {Mvvm} from './mvvm.js'
        window.vm = new Mvvm({
            el:'#app',
            data:{
                school:{
                    name:'zz',
                    aa: 22,
                    bb: 33
                },
                a:22
            },
            computed:{
                getName(){
                    return this.school.name + '呵呵';
                }
            }
        })
    </script>
</body>
</html>

Mvvm类的实现

根据脑图不难看出,Mvvm中主要做了几件事情:

1.将外部传达的参数option中抽离出el和data.单独绑到Mvvm实例上。抽离出来的主要原因是其他地方会疯狂用到data和el值。

文章中文字描述的部分所有关于data默认就是$data, el默认就是$el

2.new Observal(this); 数据劫持 -- 为什么要数据劫持呢?为了data中的值全部用Object.defineProperty增加get,set函数。(vue内部的数据拦截实现就是用的defineProperty)-- 最重要的为了实时监听data中数据的变化。get中添加订阅者(watch).set中只要值发生变化就 触发订阅者的 赋值函数。把更改的值动态的响应到页面。-- 这里看不懂没关系。一会有代码

3. 将data中的数据绑定到vm实例上一份,因为vue中直接用this.xx 等同于调用this.data.xx的属性。为了和vue保持一致。就写了这一操作。-- 你要是不喜欢这步。你可以不要,你开心就好,这步也不重要

4.编译模板 -- 就是排查el中的html 元素,其中要是有{{xx.xx}}   <input v-model="xx.xx">这种。就把它摘出来。经过一番加工。把它替换成data中的值。

根据下边的基础类,我就说这么多。下边就上述的2,3,4项。我详细的说明下

​
// 基础类
export class Mvvm{
    constructor(option){
        // option 外部传的参数
        this.$el = option.el;
        this.$data =  option.data;
        
        // 这个根元素 存在 
        if(this.$el){
            // 数据劫持,给所有的增加defineProperty -- set,get
            new Observal(this);
            
            // 将$data中的数据绑定到vm实例上一份
            this.proxVM(this, this.$data);

            // 编译模板
            new Compiler(this.$el, this);
        }
    }
}

​

 

为了方便理解--我先说编译模板,为什么要编译模板,刚才说过了。就是想把id='app'中的v-model和{{}}v-html,v-on这些乱七八糟的东西替换成data各个对应的值。(vue绑定的el为#app,所以这里主要职责就是替换#app中的内容)

编译模板 -- 代码:

 new Compiler(this.$el, this); // this.$el就是  #app,  this就是 new Mvvm实例

// 编译模板(模板编译)
class Compiler{
    constructor(el,vm){ // el 值 #app vm 就是 new Mvvm()
        this.vm = vm;  // 将vm和el 绑定到Compiler实例上是为了方便调用没有其他的意思
        // 判断el属性 是不是一个元素 如果不是元素  那就获取它 如果是元素 则直接用
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        // 把当前节点中的元素 获取到 放到内存中  (避免每次回流和重绘)
        let fragment =this.node2fragment(this.el);
     
        /*  
            fragment中的存放的就是原来#app中的内容
            编译模板 -- 把节点中的内容进行替换 
        */
        this.compile(fragment);

        // 把内容再塞到#app中,这用#app中都是替换后的内容了 
        this.el.appendChild(fragment);
    }
    // Compiler中的静态变量
    static CompilerTypeUtil = {
        getValue(vm, value){  // data vue中的data集合  value 为 v-model中的值 v-model="school.aa"
           let data = vm.$data;
           let val = value.split('.').reduce((lastObj,currentKey) =>{
                return lastObj[currentKey];
           },data);
           return val;
        },
        // 给$data中数据和vm中的数据赋值 modelValue:school.a.b  value:更新的值  obj: this.$data或vm
        setValueSecond(obj, modelValue, value){ 
            let arr =  modelValue.split('.');
            // index当前索引 arr为遍历的数组 currentKey当前元素 obj上一次返回的值
            arr.forEach((currentKey,index) =>{
                // school.a.b  -- > 证明遍历到b了。这时候直接赋值
                if( index >= arr.length -1 ){
                    obj[currentKey] = value;
                }else{
                    obj = obj[currentKey];
                }
            })
        },
        // 用来更新$data中数据和vm中的数据
        setValue(modelValue, vm, value){
            let obj = vm.$data
            this.setValueSecond(obj, modelValue, value);
            this.setValueSecond(vm, modelValue, value);
        },
        // 给输入框的value赋值
        // compileObj -- Compiler实例, value -- v-model值, node -- 指定节点
        // modelValue:"school.aa"  node: <input v-model="school.aa"/>
        model(vm, modelValue, node){ // value 值 school.aa

            // 这里一会重点说下 // 给school.aa增加观察者,如果稍后数据更新会触发回调方法,会拿新的输入框赋值
            new Watch(vm, modelValue, (newVal) => { 
                //  newVal -- 从 $data.school.aa 的值(更改后的值)
                this.setInput(node, newVal); // setInput 就是 给input的value属性 赋值

                /* 
                    这的setValue是什么意思呢? 是既给$data.school.aa更新值。又给vm.school.aa更新值。
                    这么写主要是为了vm.xx的值改了可以更新到$data.xx上、反之$data.xx的值改了可以更新到vm.xx上 
                */
                this.setValue(modelValue, vm, newVal);  
            });
            // 在v-model的input框中 绑定Inputs事件。用来更新$data中数据  input绑定input事件。每次改变以后,动态更新$data中的数据
            node.addEventListener('input', (e)=>{
                let value = e.target.value;
                this.setValue(modelValue, vm, value);
            })
            // 第一次编译的时候走这里,之后数据更新会走到watch中的回调中。就不会走到这里了
            let val = this.getValue(vm, modelValue);
            this.setInput(node, val);
        },
        // 给v-html赋值
        html(){ // node.html

        },
        // 给文本赋值
        text(vm, value){
            let textVal = this.getValue(vm, value);
            return textVal;
        },
        setInput(node,value){ // 给 文本框赋值 node -- input元素  value--input值
            node.value = value;
        },
        setText(node, text){
            node.textContent = text;
        }

    }
    // 判断是否是指令
    isDirective(name){
        // startsWith --》 es6新属性,判断是否以v-开头。
       return name.startsWith('v-');
    }
    compileEleNode(node){ // 替换v-model中内容  属性中带v-xx 的元素 例如:<input  v-model="school.aa"/>
        /* var reg = /v-model\s*=\s*\"(.+?)\"/g;
        var ss = 'v-model="2222" ssss v-model="2222" '
        ss.match(reg);  */
        // node.attributes 获取节点的属性
        // Array.from或[...xx]方式可以将 伪数组转化成数组
        Array.from(node.attributes).forEach(item =>{
            let {name,value} = item;   // item 对象, item中有name和value属性。在这只我只需要name和value的属性。
                                       // 所以用了解构赋值。name:"v-model" value:"school.name"

            if(this.isDirective(name)){ // 判断是否以v-开头,如果以v-开头,证明是自定义的属性(不是html自己的属性)
                let [direct, funName] = name.split('-');   // 拆分 v-model, split拆分 拆分成 direct 为 v funName 为 model
                // model/html   Compiler.CompilerTypeUtil.model(this.vm, value, node);
                Compiler.CompilerTypeUtil[funName](this.vm, value, node);
            }
        })
    }
    compileEleTextNode(node){ // 替换{{}}中内容
      /*   var reg = /\{\{(.+?)\}\}/g;
        var ss = '{{11}}dddd{{6666}}'
        ss.match(reg); */
        var reg = /\{\{(.+?)\}\}/g;
        let str = node.textContent; // 获取文本节点中的内容
        if(reg.test(str)){
            // 每返回一个值就会替换{{}}的位置
            let textVal = str.replace(reg,(...args)=>{
                /* args[1]  -- school.aa 或 school.bb  
                    str 为 {{school.aa}}{{school.bb}} 
                    reg -- /\{\{(.+?)\}\}/g
                    node div(文本元素)
                */
                // 因为repace是相当于循环替换,文本内容 为 {{school.aa}}{{school.bb}} replace中就会循环两次,
                // 分别将  school.aa 和 school.bb 加入观察者模式,当值发生变化时触发
                new Watch(this.vm, args[1], () =>{  
                    this.regroupReg(this.vm, str, reg, node);
                    
                });
                // 分别获取school.aa 和 school.bb 对应的值
                let textValue = Compiler.CompilerTypeUtil.text(this.vm, args[1]);
                // 返回 并替换正则匹配到的内容 -- 这是replace函数的特性
                return textValue;
            })
            // 给文本赋值
            Compiler.CompilerTypeUtil.setText(node, textVal);
        }
    }
    /* 
        一个div中有可能会存在两个{{}}, 例如{{a}}{{b}}当a发生变化。
        需要把a和b重新都替换一下。然后重新用node.textContent是改变整个div文本中的值。
    */
    regroupReg(vm, str, reg, node){
        let textVal = str.replace(reg,(...args)=>{
            let reqChild = args[1];  // school.aa 或 school.bb
            let textValue = Compiler.CompilerTypeUtil.text(vm, reqChild);
            Compiler.CompilerTypeUtil.setValue(reqChild, vm, textValue);
            return textValue;
        })
        // 给文本赋值
        Compiler.CompilerTypeUtil.setText(node, textVal);
    }
    compile(node){ // 用来编译内存中的dom节点 -- node 是 fragment
        let childNodes = node.childNodes //获取node的儿子元素
        Array.from(childNodes).forEach(ele =>{  // Array.from(childNodes)或者[...childNodes]把类数组转化成数组 遍历 
            
            if(ele.childNodes && ele.childNodes.length > 0){ // 查看有没有儿子元素。如果有继续递归
                this.compile(ele);
            }
            if(this.isElementNode(ele)){ // 验证是否元素节点
                // 编译元素节点
                this.compileEleNode(ele); // 递归编译元素节点
               
            }else if(this.isTextNode(ele)){ // 验证是不是文本节点
                // 编译文本节点
                this.compileEleTextNode(ele); // 递归编译文本节点
            }
        });
    }
    // 把节点移动到内存中
    node2fragment(node){
       /*  
            创建一个文档碎片 可以对它做添加,删除,更改 节点的操作。
            一般就是在文档碎片上添加完了 以后,在添加到html中这样可避免回流之类。
        */
        let fragment = document.createDocumentFragment();
        let firstChild;
        // node 就是 document.querySelect('#app')
        /*  
            while(firstChild = node.firstChild) 每次获取#app第一个元素(儿子节点)把这个节点放到文档碎片中  
            
            appendChild具有移动性: 每次执行完 fragment.appendChild(firstChild); 后,#app中的元素就会 少一个,
            再次用node.firstChild实际拿到的是原来#app中的第二个元素,以此类推,
            最后#app中的元素就被拿没有了。都在文档碎片(fragment)中了。
        
        */
        while(firstChild = node.firstChild){
            // appendChild具有移动性
            fragment.appendChild(firstChild);
        }
        return fragment;
        
    }
    isElementNode(node){ //是不是元素节点
        return node.nodeType === 1;
    }
    isTextNode(node){ //是不是文本节点
        return node.nodeType === 3;
    }
}

说完编译模板,我们再说下数据拦截:

// 数据拦截
class Observal{
    constructor(vm){
        this.vm = vm;
        this.data = vm.$data;
        this.setData(this.data);
    }
    setData(data){ // 给vm中的data遍历。增加get和set
        let me = this;
        if(data && typeof data == 'object'){ // data如果是对象或者是数组
            for(let key in data){
                dep = new Dep(); // 给每一个属性 都加上一个发布订阅功能(每个变量都有自己的dep)
                let val = data[key];
                Object.defineProperty(data, key, {
                    set(newVal){
                        // 当set的值得时候
                        if(val != newVal){
                            val = newVal;
                            me.setData(val);
                            dep.updateWatcher('$data'); // 属性值发生变化,就触发订阅dep中的方法
                        }
                    },
                    get(){
                        // 当得到值得时候,将属于变量的watch对象添加到dep中
                        if(Dep.target){
                            dep.sub.push(Dep.target);
                        }                           
                        return val;
                    }
                });
                this.setData(val);
            }
        }
    }
}

说数据拦截后,会发现编译模板的时候的会有new Watch(),  数据拦截中的get中有dep.sub.push(xx);  这个xx其实就是new Watch()这个就是把订阅者追加到发布订阅中。监听模板上涉及到的属性。只要值发生变化就触发dep中的updateWatcher函数

 

完整代码:

/* 
    发布订阅  -- 观察者模式  
    当监听的值发生变化,观察模式就触发开关。让模板自动编译 
*/
let dep = null;
// 发布者
class Dep{
    constructor(){
        this.sub = [];
    }
    // 订阅
    addSub(watch){
        this.sub.push(watch);
    }
    // 发布
    // 值发生改变就要触发这个函数,让所有的watch都触发
    updateWatcher(type){
        this.sub.forEach(watch =>{
            watch.nodify(type);
        })
    }
}
// 订阅者
class Watch{
    constructor(vm, modelValue, callBack){
        this.vm = vm;
        this.modelValue = modelValue;
        this.oldValue = this.getValue('$data');
        this.fn = callBack;
    }
    getValue(type){
        Dep.target = this;
        let data = null;
        if(type == '$data'){
            data = this.vm.$data;
        }else{
            data = this.vm;
        }
        let val = this.modelValue.split('.').reduce((lastObj, current) =>{
            debugger;
            return lastObj[current];
        }, data);
        Dep.target = null;
        return val;
    }
    nodify(type){
        let newVal = this.getValue(type);
        if(newVal != this.oldValue){
            this.fn(newVal);
        }
    }
}
// 数据拦截
class Observal{
    constructor(vm){
        this.vm = vm;
        this.data = vm.$data;
        this.setData(this.data);
    }
    setData(data){ // 给vm中的data遍历。增加get和set
        let me = this;
        if(data && typeof data == 'object'){ // data如果是对象或者是数组
            for(let key in data){
                dep = new Dep(); // 给每一个属性 都加上一个发布订阅功能(每个变量都有自己的dep)
                let val = data[key];
                Object.defineProperty(data, key, {
                    set(newVal){
                        // 当set的值得时候
                        if(val != newVal){
                            val = newVal;
                            me.setData(val);
                            dep.updateWatcher('$data'); // 属性值发生变化,就触发订阅dep中的方法
                        }
                    },
                    get(){
                        // 当得到值得时候,将属于变量的watch对象添加到dep中
                        debugger;
                        if(Dep.target){
                            dep.sub.push(Dep.target);
                        }                           
                        return val;
                    }
                });
                this.setData(val);
            }
        }
    }
}
// 编译模板(模板编译)
class Compiler{
    constructor(el,vm){ // el 值 #app vm 就是 new Mvvm()
        this.vm = vm;  // 将vm和el 绑定到Compiler实例上是为了方便调用没有其他的意思
        // 判断el属性 是不是一个元素 如果不是元素  那就获取它 如果是元素 则直接用
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        // 把当前节点中的元素 获取到 放到内存中  (避免每次回流和重绘)
        let fragment =this.node2fragment(this.el);
     
        /*  
            fragment中的存放的就是原来#app中的内容
            编译模板 -- 把节点中的内容进行替换 
        */
        this.compile(fragment);

        // 把内容再塞到#app中,这用#app中都是替换后的内容了 
        this.el.appendChild(fragment);
    }
    // Compiler中的静态变量
    static CompilerTypeUtil = {
        getValue(vm, value){  // data vue中的data集合  value 为 v-model中的值 v-model="school.aa"
           let data = vm.$data;
           let val = value.split('.').reduce((lastObj,currentKey) =>{
                return lastObj[currentKey];
           },data);
           return val;
        },
        // 给$data中数据和vm中的数据赋值 modelValue:school.a.b  value:更新的值  obj: this.$data或vm
        setValueSecond(obj, modelValue, value){ 
            let arr =  modelValue.split('.');
            // index当前索引 arr为遍历的数组 currentKey当前元素 obj上一次返回的值
            arr.forEach((currentKey,index) =>{
                // school.a.b  -- > 证明遍历到b了。这时候直接赋值
                if( index >= arr.length -1 ){
                    obj[currentKey] = value;
                }else{
                    obj = obj[currentKey];
                }
            })
        },
        // 用来更新$data中数据和vm中的数据
        setValue(modelValue, vm, value){
            let obj = vm.$data
            this.setValueSecond(obj, modelValue, value);
            this.setValueSecond(vm, modelValue, value);
        },
        // 给输入框的value赋值
        // compileObj -- Compiler实例, value -- v-model值, node -- 指定节点
        // modelValue:"school.aa"  node: <input v-model="school.aa"/>
        model(vm, modelValue, node){ // value 值 school.aa

            // 这里一会重点说下 // 给school.aa增加观察者,如果稍后数据更新会触发回调方法,会拿新的输入框赋值
            new Watch(vm, modelValue, (newVal) => { 
                //  newVal -- 从 $data.school.aa 的值(更改后的值)
                this.setInput(node, newVal); // setInput 就是 给input的value属性 赋值

                /* 
                    这的setValue是什么意思呢? 是既给$data.school.aa更新值。又给vm.school.aa更新值。
                    这么写主要是为了vm.xx的值改了可以更新到$data.xx上、反之$data.xx的值改了可以更新到vm.xx上 
                */
                this.setValue(modelValue, vm, newVal);  
            });
            // 在v-model的input框中 绑定Inputs事件。用来更新$data中数据  input绑定input事件。每次改变以后,动态更新$data中的数据
            node.addEventListener('input', (e)=>{
                let value = e.target.value;
                this.setValue(modelValue, vm, value);
            })
            // 第一次编译的时候走这里,之后数据更新会走到watch中的回调中。就不会走到这里了
            let val = this.getValue(vm, modelValue);
            this.setInput(node, val);
        },
        // 给v-html赋值
        html(){ // node.html

        },
        // 给文本赋值
        text(vm, value){
            let textVal = this.getValue(vm, value);
            return textVal;
        },
        setInput(node,value){ // 给 文本框赋值 node -- input元素  value--input值
            node.value = value;
        },
        setText(node, text){
            node.textContent = text;
        }

    }
    // 判断是否是指令
    isDirective(name){
        // startsWith --》 es6新属性,判断是否以v-开头。
       return name.startsWith('v-');
    }
    compileEleNode(node){ // 替换v-model中内容  属性中带v-xx 的元素 例如:<input  v-model="school.aa"/>
        /* var reg = /v-model\s*=\s*\"(.+?)\"/g;
        var ss = 'v-model="2222" ssss v-model="2222" '
        ss.match(reg);  */
        // node.attributes 获取节点的属性
        // Array.from或[...xx]方式可以将 伪数组转化成数组
        Array.from(node.attributes).forEach(item =>{
            let {name,value} = item;   // item 对象, item中有name和value属性。在这只我只需要name和value的属性。
                                       // 所以用了解构赋值。name:"v-model" value:"school.name"

            if(this.isDirective(name)){ // 判断是否以v-开头,如果以v-开头,证明是自定义的属性(不是html自己的属性)
                let [direct, funName] = name.split('-');   // 拆分 v-model, split拆分 拆分成 direct 为 v funName 为 model
                // model/html   Compiler.CompilerTypeUtil.model(this.vm, value, node);
                Compiler.CompilerTypeUtil[funName](this.vm, value, node);
            }
        })
    }
    compileEleTextNode(node){ // 替换{{}}中内容
      /*   var reg = /\{\{(.+?)\}\}/g;
        var ss = '{{11}}dddd{{6666}}'
        ss.match(reg); */
        var reg = /\{\{(.+?)\}\}/g;
        let str = node.textContent; // 获取文本节点中的内容
        if(reg.test(str)){
            // 每返回一个值就会替换{{}}的位置
            let textVal = str.replace(reg,(...args)=>{
                /* args[1]  -- school.aa 或 school.bb  
                    str 为 {{school.aa}}{{school.bb}} 
                    reg -- /\{\{(.+?)\}\}/g
                    node div(文本元素)
                */
                // 因为repace是相当于循环替换,文本内容 为 {{school.aa}}{{school.bb}} replace中就会循环两次,
                // 分别将  school.aa 和 school.bb 加入观察者模式,当值发生变化时触发
                new Watch(this.vm, args[1], () =>{  
                    this.regroupReg(this.vm, str, reg, node);
                    
                });
                // 分别获取school.aa 和 school.bb 对应的值
                let textValue = Compiler.CompilerTypeUtil.text(this.vm, args[1]);
                // 返回 并替换正则匹配到的内容 -- 这是replace函数的特性
                return textValue;
            })
            // 给文本赋值
            Compiler.CompilerTypeUtil.setText(node, textVal);
        }
    }
    /* 
        一个div中有可能会存在两个{{}}, 例如{{a}}{{b}}当a发生变化。
        需要把a和b重新都替换一下。然后重新用node.textContent是改变整个div文本中的值。
    */
    regroupReg(vm, str, reg, node){
        let textVal = str.replace(reg,(...args)=>{
            let reqChild = args[1];  // school.aa 或 school.bb
            let textValue = Compiler.CompilerTypeUtil.text(vm, reqChild);
            Compiler.CompilerTypeUtil.setValue(reqChild, vm, textValue);
            return textValue;
        })
        // 给文本赋值
        Compiler.CompilerTypeUtil.setText(node, textVal);
    }
    compile(node){ // 用来编译内存中的dom节点 -- node 是 fragment
        let childNodes = node.childNodes //获取node的儿子元素
        Array.from(childNodes).forEach(ele =>{  // Array.from(childNodes)或者[...childNodes]把类数组转化成数组 遍历 
            
            if(ele.childNodes && ele.childNodes.length > 0){ // 查看有没有儿子元素。如果有继续递归
                this.compile(ele);
            }
            if(this.isElementNode(ele)){ // 验证是否元素节点
                // 编译元素节点
                this.compileEleNode(ele); // 递归编译元素节点
               
            }else if(this.isTextNode(ele)){ // 验证是不是文本节点
                // 编译文本节点
                this.compileEleTextNode(ele); // 递归编译文本节点
            }
        });
    }
    // 把节点移动到内存中
    node2fragment(node){
       /*  
            创建一个文档碎片 可以对它做添加,删除,更改 节点的操作。
            一般就是在文档碎片上添加完了 以后,在添加到html中这样可避免回流之类。
        */
        let fragment = document.createDocumentFragment();
        let firstChild;
        // node 就是 document.querySelect('#app')
        /*  
            while(firstChild = node.firstChild) 每次获取#app第一个元素(儿子节点)把这个节点放到文档碎片中  
            
            appendChild具有移动性: 每次执行完 fragment.appendChild(firstChild); 后,#app中的元素就会 少一个,
            再次用node.firstChild实际拿到的是原来#app中的第二个元素,以此类推,
            最后#app中的元素就被拿没有了。都在文档碎片(fragment)中了。
        
        */
        while(firstChild = node.firstChild){
            // appendChild具有移动性
            fragment.appendChild(firstChild);
        }
        return fragment;
        
    }
    isElementNode(node){ //是不是元素节点
        return node.nodeType === 1;
    }
    isTextNode(node){ //是不是文本节点
        return node.nodeType === 3;
    }
}
// 基础类
export class Mvvm{
    constructor(option){
        this.$el = option.el;
        this.$data =  option.data;
        
        // 这个根元素 存在 
        if(this.$el){
            // 数据劫持,给所有的增加defineProperty -- set,get
            new Observal(this);
            
            // 将$data中的数据绑定到vm实例上一份
            this.proxVM(this, this.$data);

            // 编译模板
            new Compiler(this.$el, this);
        }
    }
    proxVM(scope, data){
        if(data && typeof data == "object"){
            for(let key in data){
                let val = data[key];
                Object.defineProperty(scope, key, {
                    get(){
                        return val;
                    },
                    set(newVal){
                        if(val != newVal){
                            val = newVal;
                            dep.updateWatcher('vm');
                        }
                    }
                });
            }
        }
    }
}

 

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="school.name">
        <input type="text" v-model="a">
        {{a}}
        <div>
                {{school.aa}}{{school.bb}}
                <span>
                    {{school.bb}}
                </span>
        </div>
    </div>
    <script type="module">
        import {Mvvm} from './mvvm.js'
        window.vm = new Mvvm({
            el:'#app',
            data:{
                school:{
                    name:'zz',
                    aa: 22,
                    bb: 33
                },
                a:22
            },
            computed:{
                getName(){
                    return this.school.name + '呵呵';
                }
            }
        })
    </script>
</html>

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值