MVVM

前置

  1. instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
  2. Array.from(obj) ,将伪数组转化为数组
  3. 节点分为文档节点,元素节点,属性节点,文本节点,其中元素节点,属性节点,文本节点的nodeType分别对应1,2,3
  4. Object.defineProperty():
    • 原文:https://www.cnblogs.com/junjun-001/p/11761252.html#commentform
    • 小记:
      • Object.defineProperty 需要三个参数(object , propName , descriptor)
      • object 对象 => 给谁加
      • propName 属性名 => 要加的属性的名字 【类型:String】
      • descriptor 属性描述 => 加的这个属性有什么样的特性【类型:Object】
    • 关于属性的描述:
      • value: 设置属性的值
      • writable: 值是否可以重写,默认为false
      • enumerable: 目标属性是否可以被枚举,默认为false
      • configurable:目标属性是否可以重新定义,默认为false
      • set: 目标属性设置值的方法
      • get:目标属性获取值的方法
  5. Object的hasOwnProperty()方法返回一个布尔值,判断对象是否包含特定的自身(非继承)属性。
  6. DocumentFragment:
    • 原文:https://www.cnblogs.com/echolun/p/10098752.html
    • 小记:
      • DocumentFragment不是真实DOM树的一部分,它的变化不会引起DOM树的重新渲染的操作(reflow) ,且不会导致性能等问题
      • 我们可以将DocumentFragment作为一个暂时的DOM节点存储器,当我们在DocumentFragment 修改完成时,我们就可以将存储DOM节点的DocumentFragment一次性加入DOM树,从而减少回流次数,达到性能优化的目的
  7. appendChild() 方法向节点添加最后一个子节点。
  8. firstChild拿到父元素节点下的子节点
    • 对于父元素节点来说,firstChild最少会执行三次,第一次拿到文本(父元素的文本首节点),中间次数拿到元素节点,最后一次拿到文本(父元素的文本末节点)
  9. childNodes:属性返回节点的子节点集合,以 NodeList 为对象,伪数组,可以使用forEach遍历
    10.attributes:属性返回节点的属性集合,伪数组,不可使用forEach遍历(不包括子节点)

数据代理

  1. 数据代理:通过一个对象来代理另一个对象的写入/读取操作
  2. VUE数据代理:通过vm对象来代理data对象中所有属性的操作
  3. 基本实现流程
    • 通过Object.defineProperty()给MVVM构造函数的实例对象添加与data对象的属性对应的属性描述符
    • 所添加的属性包含getter/setter
    • getter/setter内部去代理data中对应的数据,进而触发数据劫持

模板解析

模板解析的基本流程

  1. 将el的所有子节点取出,添加到一个新建的文档fragment对象中,(fragment不是真正的DOM结构,所以不会触发回流)
  2. 首先对fragment中的所有层次子节点进行递归,确定找到所有的子节点,文本节点进行正则判断,元素节点则进行下一步属性解析
  3. 属性解析:找到节点后,分析元素节点中绑定的属性,如果是事件指令,则绑定对应的事件,如果是一般指令,创建构造函数Watcher的实例

数据劫持

  • 创建MVVM构造函数实例的时候,执行了observe(data, this),创建了构造函数Observer的实例,并且通过函数observe的递归调用,对每一个层次的vm实例data下的属性都对应创建了一个Dep实例(a:{name:2},这种类型的数据要递归两层),直到找到所有的层次的数据,然后对vm实例对象里的$options属性中的data对象下的属性进行数据劫持(监听)

四个重要对象

  1. Observer
    • 用来对data所有属性数据进行劫持的构造函数
    • 给data中所有属性重新定义属性描述(get/set)
    • 为data中的每个属性创建对应的dep对象
  2. Dep
    • data中的每个属性(所有层次)都对应一个dep对象
    • 创建时机:
      • 初始化,创建vm实例对象时
      • 修改vm实例对象中data对象下的属性时
    • 对象的结构:{id:每个dep都具有一个唯一的ID,subs:构造函数Watcher的实例,可能不止一个,看子元素调用了几次}
    • subs属性说明
      • 当 watcher 被创建时, 内部将当前 watcher 对象添加到对应的 dep 对象的 subs 中
        • 当此 data 属性的值发生改变时, subs 中所有的 watcher 都会收到更新的通知,
  3. Compiler
    • 每解析一个表达式(非事件指令)都会创建一个对应的 watcher 对象, 并建立 watcher 与 dep 的关系
    • complie 与 watcher 关系: 一对多的关系
  4. Watcher
    • 模板中每个非事件指令或表达式都对应一个 watcher 对象
  5. 总结: dep 与 watcher 的关系: 多对多
    1. data中一个单层属性对应一个dep实例,但是一个dep里可能包含多个Watcher实例(看模板中有几个表达式使用了同一个属性)
      • 举例:
//因为someStr: 'hello'是单层属性,所以只会创建一次Dep实例,页面使用了两次{{someStr}},所以一个Dep实例里包含了两个Watcher实例
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MVVM</title>
</head>
<body>

<div id="mvvm-app">
     <p>{{someStr}}</p>
     <p>{{someStr}}</p>
</div>
<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/mvvm.js"></script>
<script>
    let vm=new MVVM({
        el: '#mvvm-app',
        data: {
            someStr: 'hello',
        },
        methods:{
            change(){
                console.log('我执行了')
            }
        }
    })
    console.log(vm)
</script>

</body>
</html>
  1. 模板中一个非事件表达式对应一个 Watcher, 两个Dep可能包含同一个Watcher实例
//someStr: {name:100}是两层属性,会创建两个Dep实例,但是页面只是用了一次someStr.name,所以Watcher实例只有一个,但是Dep实例却创建了两次
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MVVM</title>
</head>
<body>

<div id="mvvm-app">
     <p>{{someStr.name}}</p>
</div>
<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/mvvm.js"></script>
<script>
    let vm=new MVVM({
        el: '#mvvm-app',
        data: {
            someStr: {name:100},
        },
        methods:{
            change(){
                console.log('我执行了')
            }
        }
    })
    console.log(vm)
</script>

</body>
</html>

代码

  1. 补充一下断点调试方法:

    • 首先要知道在哪打断点, 因为我们要监听MVVM构造函数,所以第一步要在18行打一个断点
    • 在这里插入图片描述
    • 第二步进入MVVM构造函数内调试
    • 在这里插入图片描述
    • 第三步,找到MVVM构造函数里实现代理的方法,打断点,并进入方法内部
    • 在这里插入图片描述
    • 分析代理方法
    • 在这里插入图片描述
    • 取消断点调试
  2. 关于节点的dome

    <!DOCTYPE html>
    <html lang="en">
    <head>
    	<meta charset="UTF-8">
    	<title>Node</title>
      <div id='tag'>
          父元素的文本首节点
          <p>
              <span>#tag下的子元素一次性注入,nodeType为1</span>
          </p>
          父元素的文本末节点
      </div>
    </head>
    <body>
    	
    </body>
    <script type="text/javascript">
      //获得tag元素节点
      let tag=document.getElementById('tag')
      //定义变量fragment作为DOM节点存储器
      let fragment=document.createDocumentFragment()
      let child=''
      while (child= tag.firstChild){
        //Node.firstChild 拿到父元素节点下的子节点 
        //对于tag元素节点来说,firstChild会执行三次,第一次拿到文本(父元素的文本首节点),第二次拿到元素节点,第三次拿到文本(父元素的文本末节点)
        console.log(tag.firstChild.nodeType)//会执行三次循环 nodeType分别为 3(文本节点) 1(元素节点) 3(文本节点)
        fragment.appendChild(child)
      }
      console.log(fragment) //把tag元素节点存储到节点存储器中
    </script>
    </html>
    
  3. 综合代码分析

//html页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MVVM</title>
</head>
<body>

<div id="mvvm-app">
     <input v-on:click='change' class='some' v-model='someStr'>{{someStr}}</input> 
</div>
<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/mvvm.js"></script>
<script>
    let vm=new MVVM({
        el: '#mvvm-app',
        data: {
            someStr: 'hello',
            someStr1: {
                name:'1'
            }
        },
        methods:{
        	change(){
        		console.log('我执行了')
        	}
        }
    })
    console.log(vm)
</script>

</body>
</html>
// mvvm.js
// 声明MVVM构造函数
function MVVM(options) {
    //把参数options变为实例对象里的$options属性(对象)
    this.$options = options || {};
    //数据备份,把实例对象里的$options属性(对象)里的data属性(对象)备份到实例对象里的_data属性(对象)和变量data中
    var data = this._data = this.$options.data;
    //用变量me代替实例对象
    var me = this;
    // 数据代理
    // 实现 vm.xxx -> vm._data.xxx
    // 找到实例对象里的$options属性(对象)里的data属性(对象)里的key值
    Object.keys(data).forEach(function(key) {
        me._proxyData(key);
    });

    this._initComputed();
    //数据绑定
    //两个参数分别为:实例对象里的$options属性(对象),实例
    observe(data, this);
    //↓进入调试,vm实例对象的$compile属性(对象)为Compile构造函数的实例对象
    //传入的参数分别为 vm实例的el对象,vm实例
    this.$compile = new Compile(options.el || document.body, this)
}
//重写MVVM构造函数的显示原型对象
MVVM.prototype = {
    //构造函数MVVM(原型对象)
    constructor: MVVM,
    $watch: function(key, cb, options) {
        new Watcher(this, key, cb);
    },
    //数据代理的方法
    _proxyData: function(key, setter, getter) {
        //用变量me代替实例对象
        var me = this;
        setter = setter || 
        Object.defineProperty(me, key, {
            configurable: false,//不可重新定义
            enumerable: true,//可以枚举
            //数据代理get实现
            get: function proxyGetter() {
                //用实例对象里_data属性(对象)代理实例对象里$options属性(对象)里的data属性(对象)的读取操作
                return me._data[key];
            },
            //数据代理set实现
            set: function proxySetter(newVal) {
                //用实例对象里_data属性(对象)代理实例对象里$options属性(对象)里的data属性(对象)的写入操作
                //数据代理,改变了vm实例中_data[key]属性对象的值,触发了数据劫持函数
                me._data[key] = newVal;
            }
        });
    },

    _initComputed: function() {
        var me = this;
        var computed = this.$options.computed;
        if (typeof computed === 'object') {
            Object.keys(computed).forEach(function(key) {
                Object.defineProperty(me, key, {
                    get: typeof computed[key] === 'function' 
                            ? computed[key] 
                            : computed[key].get,
                    set: function() {}
                });
            });
        }
    }
};
//compiles
//Compile构造函数   (Compile的实例对象统称为vm.$compile)
//compiles
//Compile构造函数   (Compile的实例对象统称为vm.$compile)
function Compile(el, vm) {
    // vm.$compile实例对象的$vm属性(对象)指定为vm实例对象
    this.$vm = vm;
    // vm.$compile实例对象的$el属性(对象)指定为元素节点
    // isElementNode判断节点是否为元素节点
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    // 判断vm.$compile实例对象的$el属性(对象)是否存在
    if (this.$el) {
        // vm.$compile实例对象的$fragment 属性保存元素节点
        this.$fragment = this.node2Fragment(this.$el);
        // 初始化方法,编译fragment中所有层次的子节点
        this.init();
        //将编译好的fragment添加到vm实例的$compile对象中的el对象里
        this.$el.appendChild(this.$fragment);
    }
}
//重写Compile构造函数的显示原型对象
Compile.prototype = {
    //构造函数的原型对象
    constructor: Compile,
    //实例对象的node2Fragment方法
    node2Fragment: function(el) {
        //定义变量fragment作为DOM节点存储器
        var fragment = document.createDocumentFragment(),
        child=''
        //Node.firstChild 拿到父元素节点下的子节点 
        //对于此元素节点来说,firstChild会执行三次,第一次拿到文本(父元素的文本首节点),第二次拿到元素节点,第三次拿到文本(父元素的文本末节点)
        while (child = el.firstChild) {
            // 会执行三次循环 nodeType分别为 3(文本节点) 1(元素节点) 3(文本节点)
            fragment.appendChild(child);
        }
        return fragment;
    },
    //实例对象的init方法
    init: function() {
        this.compileElement(this.$fragment);
    },
    //实例对象的compileElement方法
    compileElement: function(el) {
        //childNodes:属性返回节点的子节点集合,以 NodeList 为对象,可以使用forEach遍历 
        // 本例中el.childNodes返回的结果为[text,p,text],但是childNodes instanceof Array为false,所以childNodes是伪数组
        var childNodes = el.childNodes,
            me = this;
        // 将childNodes转化为数组,并使用forEach遍历
        [].slice.call(childNodes).forEach(function(node) {
            // 第一次遍历 拿到的是文本节点,且文本节点为空
            // 第二次遍历,拿到的是元素节点,文本内容为{{someStr}}
            // 第三次遍历,拿到的是文本节点,且文本内容为空
            var text = node.textContent;
            //第一次遍历,不符合该正则表达式
            //第二次遍历,符合该正则表达式
            //第三次遍历,不符合该正则表达式
            var reg = /\{\{(.*)\}\}/;
            //第一次遍历,文本节点,不符合
            //第二次遍历,元素节点,符合
            //第三次遍历,文本节点,不符合
            if (me.isElementNode(node)) {
                //如果是元素节点,要进行指令判断
                me.compile(node);
            //第一次遍历,文本节点,符合,正则表达式不符合
            //第二次遍历,元素节点,不符合,正则表达式符合
            //第三次遍历,文本节点,符合,正则表达式不符合
            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.$1.trim());
            }
            //第一次遍历,文本节点,不包含子节点,node.childNodes,length属性为0
            //第二次遍历,元素节点,包含子节点,[text,text],length属性为2
            //第三次遍历,文本节点,不包含子节点,node.childNodes,length属性为0
            if (node.childNodes && node.childNodes.length) {
                //第二次遍历,元素节点会重新执行compileElement方法,通过递归实现所有层次节点的编译
                me.compileElement(node);
            }
        });
    },
    // 指令判断
    compile: function(node) {
        //attributes:属性返回节点的属性集合,伪数组,不可使用forEach遍历(不包括子节点)
        //nodeAttrs instanceof Array为false,所以nodeAttrs是伪数组
        var nodeAttrs = node.attributes,
            me = this;
        //将nodeAttrs转化为数组,并循环遍历
        [].slice.call(nodeAttrs).forEach(function(attr) {
            // 属性的名字
            //第一次循环 attrName为v-on:click
            //第二次循环 attrName为class
            //第三次循环 attrName为v-model
            var attrName = attr.name;
            //判断是否为v-开头的指令
            if (me.isDirective(attrName)) {
                //attr.value为元素节点属性自定义的value
                //第一次循环exp为change
                //第二次循环不符合v-开头
                //第三次循环exp为someStr
                var exp = attr.value;
                //截取v-开头的指令v-后面的字符串,第一次循环dir的结果为on:click
                //截取v-开头的指令v-后面的字符串,第三次循环dir的结果为model
                var dir = attrName.substring(2);
                // 判断是否为on开头的事件指令 ,第一次循环符合,第三次循环不符合
                if (me.isEventDirective(dir)) {
                    //第一次循环
                    //处理on开头的事件指令
                    //四个参数依次为:遍历选中的node元素节点,vm实例对象,change,on:click
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    //第三次循环
                    //处理其他事件指定,本例中为v-model
                    //先判断compileUtil[dir]是否存在,如果存在在执行第二部分,如果不存在就不执行了
                    //三个参数依次为:遍历选中的node元素节点,vm实例对象,someStr
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }
                //处理完成后从元素节点移除该属性
                node.removeAttribute(attrName);
            }
        });
    },

    compileText: function(node, exp) {
        compileUtil.text(node, this.$vm, exp);
    },
    //判断属性是否为v-开头
    isDirective: function(attr) {
        return attr.indexOf('v-') == 0;
    },
    //判断属性是否为on开头
    isEventDirective: function(dir) {
        return dir.indexOf('on') === 0;
    },
    // 判断节点是否为元素节点
    isElementNode: function(node) {
        return node.nodeType == 1;
    },
    //判断节点是否为文本节点
    isTextNode: function(node) {
        return node.nodeType == 3;
    }
};

// 指令处理集合
var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },

    html: function(node, vm, exp) {
        this.bind(node, vm, exp, 'html');
    },
    //三个参数依次为:遍历选中的node元素节点,vm实例对象,someStr
    //第三次循环进入
    model: function(node, vm, exp) {
        //四个参数分别为:遍历选中的node元素节点,vm实例对象,someStr,model
        this.bind(node, vm, exp, 'model');

        me = this,
        // 两个参数分别为:vm实例对象,someStr
            val = this._getVMVal(vm, exp);
        //当input框中的数据发生变化时,会触发数据劫持和数据代理的set
        node.addEventListener('input', function(e) {
            var newValue = e.target.value;
            if (val === newValue) {
                return;
            }
            //三个参数分别为:vm实例对象,somstr,输入的新值
            //属性改变最先触发的数据代理里的set,因为重新修改的是vm实例对象中的someStr属性
            me._setVMVal(vm, exp, newValue);
            val = newValue;
        });
    },

    class: function(node, vm, exp) {
        this.bind(node, vm, exp, 'class');
    },
    //四个参数分别为:遍历选中的node元素节点,vm实例对象,someStr,model
    bind: function(node, vm, exp, dir) {
        //函数赋值,updaterFn代表updater.modelUpdater(node, value, oldValue)函数
        var updaterFn = updater[dir + 'Updater'];
        //updaterFn 存在,执行第二段
        //updaterFn函数两个参数分别为:元素节点,hello
        //_getVMVal函数两个参数分别为:vm实例对象,someStr
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));
        //监听,三个参数分别为vm实例,someStr,函数
        
        new Watcher(vm, exp, function(value, oldValue) {
            //三个参数分别为:
            updaterFn && updaterFn(node, value, oldValue);
        });
    },

    // 事件处理
    // 四个参数依次为:遍历选中的node元素节点,vm实例对象,change,on:click
    eventHandler: function(node, vm, exp, dir) {
        //eventType结果为click
        var eventType = dir.split(':')[1]
        //先判断实例的$options对象的methods对象是否存在,如果存在执行第二段,判断methods对象中是否有change对象
        fn = vm.$options.methods && vm.$options.methods[exp];
        //如果eventType和fn都存在,向vm
        if (eventType && fn) {
            //addEventListener(event, function, useCapture),第一个参数事件名字,第二个参数事件的回调函数,第三个参数规定事件在冒泡阶段执行(false)还是捕获阶段执行(true)
            //bind 把一个函数放到实例对象中,fn函数放到vm实例中,点击时执行
            node.addEventListener(eventType, fn.bind(vm), false);
        }
    },
    //_getVMVal两个参数分别为:vm实例对象,someStr
    _getVMVal: function(vm, exp) {
        var val = vm;
        exp = exp.split('.');
        //采用循环的目的是 找到嵌套的属性 a.b.c
        exp.forEach(function(k) {
            //从vm实例中通过key值获得value
            val = val[k];
        });
        return val;
    },
    // 双向数据绑定
    // 三个参数分别为:vm实例对象,somstr,输入的新值
    // 属性改变最先触发的数据代理里的set,因为重新修改的是vm实例对象中的someStr属性
    _setVMVal: function(vm, exp, value) {
        var val = vm;
        
        exp = exp.split('.');
        //这里也是为了解决多层属性嵌套的问题 a.b.c
        exp.forEach(function(k, i) {
            // 非最后一个key,更新val的值
            if (i < exp.length - 1) {
                val = val[k];
            } else {
                val[k] = value;
            }
        });
    }
};

//更新节点的工具对象
var updater = {
    //更新节点的textContent属性值
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    },
    //更新节点的innerHTML属性值
    htmlUpdater: function(node, value) {
        node.innerHTML = typeof value == 'undefined' ? '' : value;
    },
    //更新节点的className属性值
    classUpdater: function(node, value, oldValue) {
        var className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');

        var space = className && String(value) ? ' ' : '';

        node.className = className + space + value;
    },
    //更新节点的value属性值
    //本例中第一个参数为遍历选中的node元素节点,第二个是通过_getVal函数获得的属性值本例中为hello,第三个?
    //更新元素节点的value值
    modelUpdater: function(node, value, oldValue) {
        node.value = typeof value == 'undefined' ? '' : value;
    }
};
//observer.js
//实现数据劫持,
//Observer构造函数
//参数为实例对象里的$options属性中的data(对象)下的属性
//本例中为{somstr:100}
function Observer(data) {
    //构造函数Observer的实例中的data属性指定为vm实例对象里的$options属性中的data对象下的属性
    this.data = data;
    //参数为vm实例对象里的$options属性中的data对象下的属性
    this.walk(data);
}
//重写显示原型
Observer.prototype = {
    constructor: Observer,
    walk: function(data) {
        var me = this;
        //找到key值和value值
        Object.keys(data).forEach(function(key) {
            //传入key值和value值,key值为someStr,value值为hello
            me.convert(key, data[key]);
        });
    },
    convert: function(key, val) {
        //参数为vm实例对象里的$options属性中的data对象下的属性,传入key值,传入的value值
        this.defineReactive(this.data, key, val);
    },
    //三个参数分别为:vm实例对象里的$options属性,传入key值,传入的value值
    defineReactive: function(data, key, val) {
        //因为会创建很多个Observer构造函数的实例所以对于构造函数Dep也会创建很多个实例
        var dep = new Dep();
        //递归调用,参数为hello,单层属性只递归一次
        var childObj = observe(val);
        //数据劫持,给vm实例对象里的$options属性中的data里的属性分别添加get和set
        //两个参数分别为:参数为vm实例对象里的$options属性中的data对象下的属性,传入key值
        Object.defineProperty(data, key, {
             // 可枚举
            enumerable: true,
            // 不能重新定义
            configurable: false, 
            get: function() {
            	//如果执行了Watcher构造函数的get方法,那么Dep.target对象就是Watcher构造函数的实例
                if (Dep.target) {
                    dep.depend();
                }
                return val;
            },
            set: function(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                // 新的值是object的话,进行监听
                childObj = observe(newVal);
                // 通过数据代理set函数触发,通知订阅者,更新页面
                dep.notify();
            }
        });
    }
};

function observe(value, vm) {
    //递归结束的条件,递归所有层次的属性,因为这条语句,所以Observer构造函数的实例会创建很多次
    //本例中只创建一次,因为someStr: 'hello',为简单的属性且只有一条
    //如果属性为someStr: {name:'hello'}会创建两个Observer构造函数实例
    if (!value || typeof value !== 'object') {
        return;
    }

    return new Observer(value);
};


var uid = 0;

function Dep() {
    this.id = uid++;
    this.subs = [];
}
//重写Dep构造函数的显示原型
Dep.prototype = {
    //参数为构造函数Watch实例对象
    addSub: function(sub) {
        //向构造函数Dep的实例的subs属性添加Watch实例对象
        this.subs.push(sub);
    },

    depend: function() {
        Dep.target.addDep(this);
    },

    removeSub: function(sub) {
        var index = this.subs.indexOf(sub);
        if (index != -1) {
            this.subs.splice(index, 1);
        }
    },
    //此时的this.subs因为已经跑了一遍get代码,所以subs里面的元素都是以id为key值,以构造函数Watcher实例为value的对象
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};

Dep.target = null;
//Watcher.js
//监听构造函数
//三个参数分别为:vm实例 ,someStr,函数对象
function Watcher(vm, expOrFn, cb) {
    //构造函数Watcher实例的cb属性指定为回调函数
    this.cb = cb;
    //构造函数Watcher实例的vm属性指定为vm实例
    this.vm = vm;
    //构造函数Watcher实例的expOrFn属性指定为someStr
    this.expOrFn = expOrFn;

    this.depIds = {};
    //判断expOrFn是否为函数
    if (typeof expOrFn === 'function') {
        this.getter = expOrFn;
    } else {
        //trim方法:去除字符串的头尾空格
        //expOrFn.trim()
        //构造函数Watcher实例的getter属性指定为一个函数
        this.getter = this.parseGetter(expOrFn.trim());
    }

    this.value = this.get();
}

Watcher.prototype = {
    constructor: Watcher,
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.get();
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    //参数为构造函数Dep实例
    addDep: function(dep) {
        // 1. 每次调用run()的时候会触发相应属性的getter
        // getter里面会触发dep.depend(),继而触发这里的addDep
        // 2. 假如相应属性的dep.id已经在当前watcher的depIds里,说明不是一个新的属性,仅仅是改变了其值而已
        // 则不需要将当前watcher添加到该属性的dep里
        // 3. 假如相应属性是新的属性,则将当前watcher添加到新属性的dep里
        // 如通过 vm.child = {name: 'a'} 改变了 child.name 的值,child.name 就是个新属性
        // 则需要将当前watcher(child.name)加入到新的 child.name 的dep里
        // 因为此时 child.name 是个新值,之前的 setter、dep 都已经失效,如果不把 watcher 加入到新的 child.name 的dep中
        // 通过 child.name = xxx 赋值的时候,对应的 watcher 就收不到通知,等于失效了
        // 4. 每个子属性的watcher在添加到子属性的dep的同时,也会添加到父属性的dep
        // 监听子属性的同时监听父属性的变更,这样,父属性改变时,子属性的watcher也能收到通知进行update
        // 这一步是在 this.get() --> this.getVMVal() 里面完成,forEach时会从父级开始取值,间接调用了它的getter
        // 触发了addDep(), 在整个forEach过程,当前wacher都会加入到每个父级过程属性的dep
        // 例如:当前watcher的是'child.child.name', 那么child, child.child, child.child.name这三个属性的dep都会加入当前watcher
        

        //判断depIds是否存在id属性
        if (!this.depIds.hasOwnProperty(dep.id)) {
            //触发构造函数dep实例的addSub方法,参数为构造函数Watcher的实例
            dep.addSub(this);
            //如果构造函数Watcher实例的depIds属性不存在dep.id,那么添加新的dep.id,如果存在,则更新dep.id对应的属性
            this.depIds[dep.id] = dep;
        }
    },
    get: function() {
        //将构造函数的target属性指定为构造函数Watcher的实例
        Dep.target = this;
        //vm实例对象被this.getter函数调用,并且传入vm实例对象作为参数
        //本例中value值为100
        var value = this.getter.call(this.vm, this.vm);
        Dep.target = null;
        return value;
    },
    //参数为somStr
    parseGetter: function(exp) {
        //不匹配字母、数字、下划线、点号和$符号,如果有直接return退出
        if (/[^\w.$]/.test(exp)) return; 
        var exps = exp.split('.');
        //参数为vm实例
        return function(obj) {
            //循环的目的是找到多层级属性对象,比如说a.b.c这种
            for (var i = 0, len = exps.length; i < len; i++) {
                if (!obj) return;
                obj = obj[exps[i]];
            }
            return obj;
        }
    }
};

MVVM原理图

在这里插入图片描述

get过程

1.首先会创建一个vm实例,创建vm实例的同时执行了MVVM构造函数
2. MVVM构造函数里执行了observe()函数,创建了构造函数Compile的实例,并且对vm.data对象里的数据的每一项进行了数据代理(注意不是每一层)
3. 第二步执行了observe()函数,通过递归调用为vm.data对象里每一层的数据都创建了一个构造函数Observer的实例和构造函数Dep的实例,并且对每一层进行了数据劫持(监听)
4. 解析模板编译,首先解析DOM结构,然后解析DOM结构上绑定的属性,如果符合模板解析条件,则创建构造函数Watcher的实例
5. 在构造函数Watcher中,存在对vm.data对象里数据的读取操作,所以执行了MVVM构造函数里get函数,而执行get函数又会执行Observer构造函数里的数据劫持
6.通过get数据劫持函数,把当前的构造函数Dep的实例保存在构造函数Watcher实例的depIds对象下
7. 向页面插入已完成的documentFargment,完成渲染

set过程

  1. 在执行set前已经跑了一遍get流程
  2. 先执行的是_setVMVal方法,改变了vm.data里对应的属性值
  3. 属性值改变,首先会触发MVVM里数据代理的set函数
  4. 数据代理里set函数改变了_data中的数据,进而触发了构造函数Observer里的数据劫持set函数
  5. 构造函数Observer里的数据劫持set函数,通知了订阅者,并且又重新读取了新值,相当于重新执行一遍get流程
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值