目录
1 用法
用法:vm.$watch( expOrFn , callback , [ options ] );
用于观察一个表达式或computed函数在vue.js实例上的变化。回调函数调用时,会从参数得到新数据和旧数据。表达式只接受以点分割的路径,例如a.b.c。比较复杂的表达式,可以用函数代替表达式。
var unwatch = vm.$watch("a", (newVal, oldVal) => {});
// 之后取消观察
unwatch();
vm.$watch返回一个取消观察函数,用来停止触发回调。
deep:为了发现对象内部值的变化,可以在选项参数中指定deep:true;
immediate:在选项参数中指定immediate:true,将立即以表达式的当前值触发回调;
2 watch的内部原理
vm.$watch其实就是对Watcher的一种封装。通过Watcher完全可以实现vm.$watch的功能,但是vm.$watch中的参数deep和immediate是Watcher中所没有的。
Vue.prototype.$watch = function (expOrFn, cb, options) {
const vm = this;
options = options || {};
const watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
cb.call(vm, watcher.value);
}
return function unwatchFn() {
watcher.teardown();
};
};
export default class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
this.deps = []; // 新增
this.depIds = new Set(); // 新增
// expOrFn参数支持函数
// 如果expOrFn是函数,则直接将它赋值给getter;
// 如果不是函数,再使用parsePath函数来读取keypath中的数据;
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)) {
// 用来记录当前Watcher已经订阅了这个Dep
this.depIds.add(id);
// 记录自己都订阅了哪些Dep
this.deps.push(dep);
// 触发将自己订阅到Dep中
dep.addSub(this);
}
}
// ...
}
当expOrFn是函数时,会发生很神奇的事情。它不只可以动态返回数据,其中读取的所有数据也都会被Watcher观察。当expOrFn是函数时,Watcher会同时观察expOrFn函数中读取的所有vue.js实例上的响应式数据。也就是说,如果函数从vue.js实例上读取了两个数据,那么Watcher会同时观察这两个数据的变化,当其中任意一个发生变化时,Watcher都会得到通知。
取消观察,本质上是把watcher实例从当前正在观察的状态的依赖列表中移除。
在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) // 新增
}
}
// ...
}
此时,Dep会记录数据发生变化时,需要通知哪些Watcher。而Watcher中也同样记录了自己会被哪些Dep通知。Watcher与Dep的关系如下图所示:
为什么是多对多的关系?Watcher每次只读一个数据,不是应该只有一个Dep吗?
其实不是。如果Watcher中的expOrFn参数是一个表达式,那么肯定只收集一个Dep,并且大部分都是这样。但凡事总有例外,expOrFn可以是一个函数,此时如果该函数中使用了多个数据,那么这时Watcher就要收集多个Dep了,例如:
this.$watch(function (){
return this.name + this.age
},function (newValue,oldValue){
console.log(newValue,oldValue)
})
如果表达式是一个函数,并且函数中访问了name和age两个数据,这种清情况下Watcher内部收集两个Dep —— name和age的Dep,同时这两个Dep中也会收集Watcher,这导致age和name中的任意一个数据发生变化时,Watcher都会收到通知。
当我们已经在Watcher中记录自己订阅了哪些Dep之后,就可以在Watcher中新增teardown方法来通知这些订阅的Dep,让它们把自己从依赖列表中移除掉:
/**
* 从所有依赖项的Dep列表中将自己移除
*/
teardown(){
let i = this.deps.length
while(i--){
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)
}
}
// ...
}
unwatch原理:把Watcher从sub中删除掉,然后当数据发生变化时,将不再通知这个已经删除的Watcher。
3 deep参数的实现原理
想要实现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 = []
// ...
}
get(){
window.target = this
let value = this.getter.call(vm,vm)
// 新增
if(this.deep){
traverse(value)
}
window.target = undefined
return value
}
// ...
}
在上面的代码中,如果用户使用了deep参数,则在window.target = undefined之前带用traverse来处理deep的逻辑。
接下来,要递归value的所有子值来触发它们收集依赖的功能:
const seenObjects = new Set()
export function traverse(val){
_tranverse(val,seenObjects)
seenObjects.clear()
}
function _tranverse(val,seen){
/**
* 而_tranverse函数其实是一个递归操作,所以这个value的子值也会触发同样的逻辑,
* 这样就可以实现通过deep参数来监听所有子值的变化
*/
let i,keys
const isA = Array.isArray(val)
if((!isA && !isObject(val)) || Object.isFrozen(val)){
// 先判断val的类型,如果它不是Array和Object,或者已经被冻结,那什么也不干
return
}
if(val.__ob__){
// 拿到val的dep.id,用这个id来保证不会重复收集依赖
const depId = val.__ob__.dep.id
if(seen.has(depId)){
return
}
seen.add(depId)
}
// 重点!!!如果是Object类型,则循环Object中所有的key,然后执行一次读取操作,再递归子值
if(isA){
i = val.length
while(i--) _tranverse(val[i],seen)
}else{
/**
* val[keys[i]]会触发getter,也就是说会触发收集依赖的操作,这时window.target还没被清空,
* 会将当前的Watcher收集进去。
*/
keys = Object.keys(val)
i = keys.length
while(i--) _tranverse(val(val[keys[i]],seen))
}
}
注:本文章来自于《深入浅出vue.js》(人民邮电出版社)阅读后的笔记整理