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