0
点赞
收藏
分享

微信扫一扫

Vue源码分析-数据劫持


数据劫持(数据绑定)

observer

  1. 给配置对象data对象中的数据进行劫持
  2. 给data中的每个属性重新定义get和set
  3. 为data中每个属性创建dep对象

compile

在解析表达式的时候会创建对应的watcher对象,并建立watcher与dep的关系,

dep

  1. 在进行数据劫持时,给data每个属性都对应一个dep对象
  2. 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);
}
},

 

双向数据绑定

  1. 在解析v-model指令的时候,会给当前元素绑定input事件监听
  2. 当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;
}
});
},

 

举报

相关推荐

0 条评论