数据劫持(数据绑定)
observer
- 给配置对象data对象中的数据进行劫持
- 给data中的每个属性重新定义get和set
- 为data中每个属性创建dep对象
compile
在解析表达式的时候会创建对应的watcher对象,并建立watcher与dep的关系,
dep
- 在进行数据劫持时,给data每个属性都对应一个dep对象
- dep对象结构:id – 每个dep对象的唯一标识,subs—包含多个watcher对象的数组
特殊说明:当模板编译时,每个表达式编译会创建watcher对象,会将当前watcher对象添加到dep对象的subs中,当data属性改变时,会通知dep对象sub中的所有watcher,最终去更新页面
watcher
当编译模板时,模板中非事件指令或表达式都对应一个watcher对象
对象组成{
vm--vm对象
exp --对应指令的表达式
cb--当表达式对应的数据发生改变时,执行的回调,会更显页面显示
value – 表达式对应的值
depIds – 表达式对应的dep对象集合
}
dep与watcher的关系:多对多
在模板中一个表达式使用多次,一个dep对象对应多个watcher对象(表达式使用的次数)
表达式是对象点的形式,1个watcher对象对应多个dep对象(表达式包含data属性的数量)
相关代码
//mvvm.js observe(data,this) //observe-观察者 |
//observe.js
function observe(value, vm) { // 判断值是否是对象,只有对象才需要变成响应式 if (!value || typeof value !== "object") { return; } return new Observer(value); }
function Observer(data) { // 存储data this.data = data; // 开始干活 this.walk(data); }
walk: function (data) { var me = this; // 遍历第一层属性 Object.keys(data).forEach(function (key) { // me.convert(key, data[key]);--无用 // 将数据重新定义成响应式数据 me.defineReactive(data, key, data[key]); }); },
convert: function (key, val) { this.defineReactive(this.data, key, val); },
//定义响应式数据 defineReactive: function (data, key, val) { // 创建dep对象 // 所有响应式数据(data)都会有一个唯一的dep对象 // dep对象会通过闭包的方式保存响应式数据的getter和setter中 var dep = new Dep(); // 因为遍历只能遍历第一层属性,如果属性值是对象,还需要再进行响应式处理 // 进行隐式递归调用, 将data上所有属性都会变成响应式属性 var childObj = observe(val);
// 将data上数据定义成响应式 Object.defineProperty(data, key, { enumerable: true, // 可枚举 configurable: false, // 不能再define get: function () { // 判断 Dep.target --> 当前有没有要处理的watcher,在创建watcher对象的时候,来执行get方法 if (Dep.target) { // 建立 dep 和 watcher 的联系 dep.depend(); } return val; }, set: function (newVal) { if (newVal === val) { return; } // 更新值 val = newVal; // 新的值是object的话,又要改成响应式数据 childObj = observe(newVal); // 通知订阅者 // 通过watcher更新用户界面 dep.notify(); }, }); },
|
//Dep对象
var uid = 0;
function Dep() { this.id = uid++; this.subs = []; }
Dep.prototype = { addSub: function (sub) { this.subs.push(sub); },
depend: function () { // 给watcher添加dep // watcher.addDep(dep) Dep.target.addDep(this); },
removeSub: function (sub) { var index = this.subs.indexOf(sub); if (index != -1) { this.subs.splice(index, 1); } },
notify: function () { // 遍历所有watcher this.subs.forEach(function (sub) { // 调用watcher的更新方法 sub.update(); }); }, };
Dep.target = null;
|
watcher.js function Watcher(vm, expOrFn, cb) { //cb—updaterFn ,更新页面的函数 this.cb = cb; this.vm = vm; // expOrFn—获取vm表达式对应值的函数 this.expOrFn = expOrFn; this.depIds = {};
if (typeof expOrFn === "function") { this.getter = expOrFn; } else { this.getter = this.parseGetter(expOrFn.trim()); } //get方法内部调用this.getter,获取表达式对应的值 this.value = this.get(); }
get: function () { //当前watcher对象作为Dep.target的值,先创建实现响应式数据,创建Dep对象,然后模板辨析,创建watcher对象 Dep.target = this; var value = this.getter.call(this.vm, this.vm); Dep.target = null; return value; }, parseGetter: function (exp) { if (/[^\w.$]/.test(exp)) return; var exps = exp.split("."); return function (obj) { for (var i = 0, len = exps.length; i < len; i++) { if (!obj) return; //取值,执行get方法,创建dep对象 obj = obj[exps[i]]; } return obj; }; },
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
// 判断有没有保存过dep(通过id判断) if (!this.depIds.hasOwnProperty(dep.id)) { // 给dep保存watcher // 为什么给dep保存watcher? // 因为watcher有更新用户界面的方法,当dep对应的响应式数据发生变化, // 就能通过dep找到所有的watcher,从而更新用户界面 dep.addSub(this); // 给watcher保存dep // 为什么给watcher保存dep? // 防止dep保存多次同一个watcher this.depIds[dep.id] = dep; // 为什么dep保存watcher用subs数组?而watcher保存dep用depIds对象? // this.depIds = { 0: dep0, 1: dep1 } 对象查找属性比数组遍历查找值快的多 // this.depIds = [0, 1] 就需要遍历,相当于要遍历所有元素 } },
|
更新页面的操作 update: function () { this.run(); }, run: function () { // 读取属性最新的值 var value = this.get(); // 读取属性上一次的值 var oldVal = this.value; // 如果值相等,就不更新 if (value !== oldVal) { // 将最新的值保存起来 this.value = value; // 调用更新用户界面的方法cb去更新 this.cb.call(this.vm, value, oldVal); } },
|
双向数据绑定
- 在解析v-model指令的时候,会给当前元素绑定input事件监听
- 当input事件执行的时候,会将最新的值赋值给vm实例身上对应的属性,触发属性的set方法,通知dep对象subs中所有watcher,从而去更新页面
相关代码
var me = this, // 读取当前属性的值 val = this._getVMVal(vm, exp); // 绑定input事件,监听元素的value的变化 node.addEventListener("input", function (e) { // 获取当前元素最新的值 var newValue = e.target.value; // 如果相等就不更新 if (val === newValue) { return; } // 更新data数据 --> 触发setter方法,从而更新用户界面 me._setVMVal(vm, exp, newValue); // 将当前值存起来,方便进行下一次比较~ val = newValue; });
|
_getVMVal: function (vm, exp) { var val = vm; exp = exp.split("."); // ['wife', 'name'] // 第一次遍历 val = vm['wife'] = {xxx} // 第二次遍历 val = {xxx}['name'] = 'rose' exp.forEach(function (k) { val = val[k]; }); return val; // 'rose' },
// 设置vm上对应表达式的值 _setVMVal: function (vm, exp, value) { var val = vm; exp = exp.split("."); exp.forEach(function (k, i) { // 非最后一个key,更新val的值 if (i < exp.length - 1) { val = val[k]; } else { val[k] = value; } }); },
|