前置
- instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
- Array.from(obj) ,将伪数组转化为数组
- 节点分为文档节点,元素节点,属性节点,文本节点,其中元素节点,属性节点,文本节点的nodeType分别对应1,2,3
- 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:目标属性获取值的方法
- Object的hasOwnProperty()方法返回一个布尔值,判断对象是否包含特定的自身(非继承)属性。
- DocumentFragment:
- 原文:https://www.cnblogs.com/echolun/p/10098752.html
- 小记:
- DocumentFragment不是真实DOM树的一部分,它的变化不会引起DOM树的重新渲染的操作(reflow) ,且不会导致性能等问题
- 我们可以将DocumentFragment作为一个暂时的DOM节点存储器,当我们在DocumentFragment 修改完成时,我们就可以将存储DOM节点的DocumentFragment一次性加入DOM树,从而减少回流次数,达到性能优化的目的
- appendChild() 方法向节点添加最后一个子节点。
- firstChild拿到父元素节点下的子节点
- 对于父元素节点来说,firstChild最少会执行三次,第一次拿到文本(父元素的文本首节点),中间次数拿到元素节点,最后一次拿到文本(父元素的文本末节点)
- childNodes:属性返回节点的子节点集合,以 NodeList 为对象,伪数组,可以使用forEach遍历
10.attributes:属性返回节点的属性集合,伪数组,不可使用forEach遍历(不包括子节点)
数据代理
- 数据代理:通过一个对象来代理另一个对象的写入/读取操作
- VUE数据代理:通过vm对象来代理data对象中所有属性的操作
- 基本实现流程
- 通过Object.defineProperty()给MVVM构造函数的实例对象添加与data对象的属性对应的属性描述符
- 所添加的属性包含getter/setter
- getter/setter内部去代理data中对应的数据,进而触发数据劫持
模板解析
模板解析的基本流程
- 将el的所有子节点取出,添加到一个新建的文档fragment对象中,(fragment不是真正的DOM结构,所以不会触发回流)
- 首先对fragment中的所有层次子节点进行递归,确定找到所有的子节点,文本节点进行正则判断,元素节点则进行下一步属性解析
- 属性解析:找到节点后,分析元素节点中绑定的属性,如果是事件指令,则绑定对应的事件,如果是一般指令,创建构造函数Watcher的实例
数据劫持
- 创建MVVM构造函数实例的时候,执行了observe(data, this),创建了构造函数Observer的实例,并且通过函数observe的递归调用,对每一个层次的vm实例data下的属性都对应创建了一个Dep实例(a:{name:2},这种类型的数据要递归两层),直到找到所有的层次的数据,然后对vm实例对象里的$options属性中的data对象下的属性进行数据劫持(监听)
四个重要对象
- Observer
- 用来对data所有属性数据进行劫持的构造函数
- 给data中所有属性重新定义属性描述(get/set)
- 为data中的每个属性创建对应的dep对象
- Dep
- data中的每个属性(所有层次)都对应一个dep对象
- 创建时机:
- 初始化,创建vm实例对象时
- 修改vm实例对象中data对象下的属性时
- 对象的结构:{id:每个dep都具有一个唯一的ID,subs:构造函数Watcher的实例,可能不止一个,看子元素调用了几次}
- subs属性说明
- 当 watcher 被创建时, 内部将当前 watcher 对象添加到对应的 dep 对象的 subs 中
-
- 当此 data 属性的值发生改变时, subs 中所有的 watcher 都会收到更新的通知,
- Compiler
- 每解析一个表达式(非事件指令)都会创建一个对应的 watcher 对象, 并建立 watcher 与 dep 的关系
- complie 与 watcher 关系: 一对多的关系
- Watcher
- 模板中每个非事件指令或表达式都对应一个 watcher 对象
- 总结: dep 与 watcher 的关系: 多对多
- data中一个单层属性对应一个dep实例,但是一个dep里可能包含多个Watcher实例(看模板中有几个表达式使用了同一个属性)
- 举例:
- 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>
- 模板中一个非事件表达式对应一个 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>
代码
-
补充一下断点调试方法:
- 首先要知道在哪打断点, 因为我们要监听MVVM构造函数,所以第一步要在18行打一个断点
- 第二步进入MVVM构造函数内调试
- 第三步,找到MVVM构造函数里实现代理的方法,打断点,并进入方法内部
- 分析代理方法
- 取消断点调试
-
关于节点的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>
-
综合代码分析
//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过程
- 在执行set前已经跑了一遍get流程
- 先执行的是_setVMVal方法,改变了vm.data里对应的属性值
- 属性值改变,首先会触发MVVM里数据代理的set函数
- 数据代理里set函数改变了_data中的数据,进而触发了构造函数Observer里的数据劫持set函数
- 构造函数Observer里的数据劫持set函数,通知了订阅者,并且又重新读取了新值,相当于重新执行一遍get流程