(1)用法
vm.$watch(expOrFn,callback,[options])
(2)参数
1、{string | Function} expOrFn
2、{Function | Object} callback
3、{Object} [options]
{boolean} deep
{boolean} immediate
(3)返回值
{Function} unwatch
(4)用法
1、用于观察一个表达式或computed函数在Vue.js实例上的变化。
2、回调函数调用时,会从参数得到新数据(new value)和旧数据(old value)。
3、表达式只接受以点分隔的路径,例如a.b.c。如果是一个比较复杂的表达式,可以用函数代替表达式。
vm.$watch('a.b.c',function(newVal,oldVal){
//做点什么
})
4、vm.$watch返回一个取消观察函数,用来停止触发回调
var unwatch = vm.$watch('a.b.c',function(newVal,oldVal){//做点什么})
//之后取消观察
unwatch();
(5)[options]选项deep
1、为了发现对象内部值的变化,可以在选项参数中指定deep:true
vm.$watch('someObject',callback,{
deep:true
})
vm.someObject.nestedValue = 123;
//回调函数将被触发
2、注意:监听数组的变动不需要这么做。
(6)[options]选项immediate
1、在选项参数中指定immediate:true,将立即以表达式的当前值触发回调
vm.$watch('a',callback,{
immediate:true
})
//立即以'a'的当前值触发回调
(1)vm.$watch其实是对Watcher的一种封装,通过Watcher完全可以实现vm.$watch的功能,但vm.$watch中的参数deep和immediate是Watcher中所没有的。
(2)实现
Vue.prototype.$watch = function(expOrFn,cb,options){
const vm = this;
options = options || {}
const watcher = new Watcher(expOrFn,cb,options){
if(options.immediate){
cb.call(vm,watcher.value)
}
return function unwatchFn(){
watcher.teardown()
}
}
}
1、执行new Watcher来实现vm.$watch的基本功能。
2、expOrFn是支持函数的,前面未做介绍。需修改Watcher
export default class Watcher{
constructor(vm,expOrFn,cb){
this.vm = vm;
//执行this.getter()就可以读取data.a.b.c的内容
if(typeof expOrFn === 'function'){
this.getter = expOrFn;
}else{
this.getter = parsePath(expOrFn);
}
this.cb = cb;
// 保存旧值
this.value = this.get();
}
\\......
}
3、新增可判断expOrFn类型的逻辑。如果expOrFn是函数,则直接将它赋值给getter;如果不是函数,再使用parsePath函数来读取keypath中的数据。(keypath指的是属性路径,例如a.b.c.d就是一个keypath,说明从vm.a.b.c.d中读取数据)。
4、当expOrFn是函数时,它不只可以动态返回数据,其中读取的所有数据也会被Watcher观察。
5、当expOrFn是字符串类型的keypath时,Watcher会读取这个keypath所指向的数据并观察这个数据的变化。而当expOrFn是函数时,Watcher会同时观察expOrFn函数中读取的所有Vue.js实例上上的响应式数据。也就是说,如果函数从Vue.js实例上读取了两个数据,那么Watcher会同时观察这两个数据的变化,当其中任意一个发生变化时,Watcher都会得到通知。
6、事实上,Vue.js中计算属性(Computed)的实现原理与expOrFn支持函数有很大的关系。
7、执行 new Watcher后,代码会判断用户是否使用了immediate参数,如果使用了,则立即执行一次cb。
8、最后,返回一个函数unwatchFn。其作用是取消观察数据。当用户执行这个函数时,实例上是执行了watcher.teardown()来取消观察数据,其本质是把watcher实例从当前正在观察的状态的依赖列表中移除。
(3)Watcher的teardown方法
1、首先需要在Watcher中记录自己订阅了谁,也就是watcher实例被收集进了哪些Dep里
2、然后当Watcher不想继续订阅这些Dep时,循环自己记录的订阅列表来通知它们(Dep)将自己从它们(Dep)的依赖列表中移除掉。
3、先在Watcher中添加addDep方法,该方法的作用是在Watcher中记录自己都订阅过哪些Dep。
export default class Watcher{
constructor(vm,expOrFn,cb){
this.vm = vm;
//新增
this.deps = [];
//新增
this.depIds = new Set()
if(typeof expOrFn === 'function'){
this.getter = expOrFn;
}else{
this.getter = parsePath(expOrFn);
}
this.cb = cb;
this.value = this.get();
}
//......
addDep(dep){
const id = dep.id;
if(!this.depIds.has(id)){
this.depIds.add(id);
this.deps.push(dep);
dep.addSub(this);
}
}
//....
}
1)使用depIds来判断如果当前Watcher已经订阅了该Dep,则不会重复订阅。
2)Watcher读取value时,会触发收集依赖的逻辑。当依赖发生变化时,会通知Watcher重新读取最新的数据。如果没有这个判断,就会发现每次数据发生了变化,Watcher都会读取最新的数据。而读数据就会再次收集依赖,这就会导致Dep中依赖有重复。这样当数据发生变化时,会同时通知多个Watcher。为了避免这个问题,只有第一次触发getter的时候才会收集依赖。
3)执行this.depIds.add来记录当前Watcher已经订阅了这个Dep。
4)执行this.deps.push(dep)记录自己都订阅了哪些Dep。
5)触发dep.addSub(this),来将自己订阅到Dep中。
(4)Watcher中新增addDep方法后,Dep中收集依赖的逻辑也需要有所改变。
//新增
let uid = 0;
export default class Dep{
constructor(){
//新增
this.id = uid++;
this.subs = [];
}
//....
depend(){
if(window.target){
//废弃
// this.addSub(window.target);
//新增
window.target.addDep(this);
}
}
//...
}
1)此时,Dep会记录数据发生变化时,需要通知哪些Watcher,而Watcher中也同样记录了自己会被哪些Dep通知。(多对多关系)
2)如果Watcher中的expOrFn参数是一个表达式,那么肯定只收集一个Dep,并且大部分都是这样。
3)但expOrFn可以是一个函数,此时如果该函数中使用了多个数据,那么这时Watcher就要收集多个Dep了
this.$watch(function(){
return this.name + this.age
},function(newValue,oldValue){
console.lognewValue,oldValue()
})
1、上面这个例子,表达式是一个函数,并且在函数中访问了name和age两个数据。
2、这种情况下,Watcher内部会收集两个Dep---name的Dep和age的Dep。
3、同时这两个Dep中也会收集Watcher。这导致age和name中任意一个数据发生变化时,Watcher都会收到通知。
4、已经在Watcher中记录自己订阅了哪些Dep之后,就可以在Watcher中新增teardown方法来通知订阅的Dep,让它们把自己从依赖列表中移除掉。
//从所有依赖项的Dep列表中将自己移除
teardown(){
let i = this.deps.length;
while(1--){
this.deps[i].removeSub(this);
}
}
export default class Dep{
//....
removeSub(sub){
const index = this.subs.indexOf(sub);
if(index>-1){
return this.subs.splice(index,1)
}
}
//...
}
1)Watcher从sub中删除掉,然后当数据发生变化时,将不再通知这个已经删除的Watcher,这就是unwatch的原理。
(1)Watcher想监听某个数据,就会触发某个数据收集依赖的逻辑,将自己收集进去,然后当它发生变化时,就会通知Watcher。
(2)要想实现deep的功能,其实就是除了要触发当前这个被监听数据的收集依赖的逻辑之外,还要把当前监听的这个值在内的所有子值都触发一遍收集依赖逻辑。这就可以实现当前这个依赖的所有子数据发生变化时,通知当前Watcher。
export default class Watcher{
constructor(vm,expOrFn,cb,options){
this.vm = vm;
//新增
if(options){
this.deep = !!options.deep;
}else{
this.deep = false;
}
this.deps = [];
this.depIds = new Set()
if(typeof expOrFn === 'function'){
this.getter = expOrFn;
}else{
this.getter = parsePath(expOrFn);
}
this.cb = cb;
this.value = this.get();
}
get(){
// 将this赋值给window.target,用于主动将自己添加到依赖中
window.target = this;
//触发了getter,触发收集依赖,将this主动添加到keypath的Dep中
let value = this.getter.call(this.vm,this.vm);
//新增
if(this.deep){
traverse(value);
}
window.target = undefined;
return value;
}
//.....
}
1、如果用户使用了deep参数,则在window.target = undefined之前调用traverse来处理deep的逻辑,这样才能保证子集收集的依赖是当前这个Watcher。若在其之后,那么其实当前的Watcher并不会被收集到子值的依赖列表中,也就无法实现deep功能。
(3)递归value的所有子值来触发它们收集依赖的功能。
const seenObjects = new Set();
export function traverse(val){
_traverse(val,seenObjects);
seenObjects.clear();
}
function _traverse(val,seen){
let i,keys;
const isA = Array.isArray(val);
if(!isA&&!isObejct(val)||Object.isFrozen(val)){
return;
}
if(val._ob_){
const depId = val._ob_.dep.id;
if(seen.has(depId)){
return
}
seen.add(depId)
}{
if(isA){
i = val.length;
while(i--) _traverse(val[i],seen);
}else{
keys = Object.keys(val);
i = keys.length;
while(i--) _traverse(val[keys[i],seen])
}
}
1、先判val类型,如果它不是Array和Object,或者已经被冻结,那么直接返回,什么也不干。
2、然后拿到val的dep.id,用这个id来保证不会重复收集依赖。
3、如果是数组,则循环数组,将数组中每一项递归调用_traverse。
4、如果是Object类型的数据,则循环Object中所有key,然后执行一次读取操作,再递归子值
_traverse(val[keys[i],seen])
1)其中val[keys[i]会触发getter,也就是说会触发收集依赖的操作,这时window.target还没有被清空,会将当前Watcher收集进去。
2)_traverse函数其实是一个递归操作,所以这个value的子值也会触发同样的逻辑,这样就可以实现通过deep参数来监听所有子值的变化。