本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2022-02-19
用于观察一个表达式或computed
函数在 Vue.js 实例上的变化。
vm.$watch(exprOrFn, callback, [options])
表达式只接受以点分割的路径,例如a.b.c
,如果是一个比较复杂的表达式,可以使用函数代替表达式。
vm.$watch
返回一个请取消观察函数,用来停止触发回调。
var unwatch = vm.$watch('a', (newVal, oldVal) => {})
unwatch();
options 选项有两个,deep
和 immediate
。
true
为深度监测。示例代码:
Vue.prototype.$watch = function (exprOrFn, cb, options) {
const vm = this;
options = options || {};
const watcher = new Watcher(vm, exprOrFn, cb, options);
if (options.immediate) {
cb.call(vm, watch.value); // 立即执行必然没有oldVal
}
return function unwatchFn() {
watcher.teardown();
}
}
Watcher.js
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
// 新增判断代码
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn;
} else {
this.getter = parsePath(expr);
}
this.cb = cb;
this.value = this.get();
}
}
当 exprOrFn 是字符串类型的 keypath 时, Watcher 会读取这个 keypath 所指向的数据并观察这个数据的变化。
而当 exprOrFn 是函数(如计算属性)时,Watcher 会同时观察 exprOrFn 函数中读取的所有 Vue.js 实例上的响应式数据,也就是说,如果函数从 Vue.js 实例上读取了两个数据(触发了两个数据的getter,Watcher实例被两个Dep收集),那么 Watcher 会同时观察这两个数据的变化,当其中任意一个发生变化时,Watcher都会得到通知。
而 Watcher 则需要记录自己都订阅了谁,也就是 Watcher 实例被收集到了哪些 Dep 里,然后当 Watcher 不想继续订阅这些 Dep 时,循环自己记录的订阅列表来通知它们(Dep)将自己从它们(Dep)的依赖列表中移除掉。
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.deps = [];
this.depIds = new Set();
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn;
} else {
this.getter = parsePath(expr);
}
this.cb = cb;
this.value = this.get();
}
addDep(dep) { // 具体解析见下文
const id = dep.id;
if (!this.deps.has(id)) {
this.depIds.add(id);
this.deps.push(dep);
dep.addSub(this);
}
}
}
上一节的实现里面,每当数据发生变化就会读取数据,而读数据就会再次收集依赖,这就会导致 Dep 中的依赖有重复。这样当数据发生变化时,会同时通知多个 Watcher。为了避免这个问题,只有第一次触发 getter 的时候才会触发依赖。
只有当 Watcher 本身被这个 Dep 订阅过,就通过 this.depIds.add
和 this.deps.push(dep)
来记录这个Dep.id和Dep,避免被统一Dep触发订阅。最后触发 dep.addSub(this)
来让完成 Dep 对 Watcher 的订阅。
Dep
收集依赖的逻辑也需要有所改变:
let uid = 0;
class Dep {
constructor () {
this.id = uid++;
this.subs = [];
}
depend() {
if (window.target) {
// 将订阅的逻辑交给Watcher进行判断
// 保证一个数据的Dep里面的同一个Watch只会被收集一次
window.target.addDep(this);
}
}
}
因此 Dep 会记录数据发生变化时,需要通知哪些 Watcher,而 Watcher 中也同样记录了自己会被哪些 Dep 通知,其实它们是多对多的关系。因为当 Watch 的是一个 computed 时,Watcher 就可能会访问多个数据。因此这个 Watcher 就会收集多个 Dep,同时这些 Dep 中也会收集该 Watch,这样任意一个数据变化,Watcher 都会收到通知。
this.$watch(function() {
return this.name + this.age;
}, function(newVal, oldVal) {
console.log(newVal);
console.log(oldVal);
})
接下来,需要实现 teardown
方法,用来通知 Dep ,让它们把自己(Watcher实例)从依赖列表中移除掉。
class Watcher {
// ...
// 从所有依赖项的Dep列表中将自己移除
teardown() {
let i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
}
}
// Dep
class Dep {
removeSub(sub) {
const index = this.subs.indexOf(sub);
if (index > -1) {
return this.subs.splice(index, 1);
}
}
}
Watcher 想监听某个数据,就会访问某个数据,触发它的getter,将自己收集进去,然后当它发生变化时,就会通知 Watcher。
要想实现 deep 的功能,其实就是除了要触发当前这个被监听数据的收集依赖的逻辑,还要将当前监听的这个值在内的所有子属性都触发一遍依赖收集。
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm;
if (options) {
this.deep = !!options.deep;
} else {
this.deep = false;
}
this.deps = [];
this.depIds = new Set();
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn;
} else {
this.getter = parsePath(expr);
}
this.cb = cb;
this.value = this.get();
}
get() {
window.target = this;
let value = this.getter.call(this.vm, this.vm)
if (this.deep) {
// 将当前Watcher收集到子值的依赖列表中
traverse(value)
}
window.target = undefined;
return 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);
// 如果不是数组并且不是对象 或者已经被冻结 就直接return
if ((!isA && !isObject(val)) || Object.isFrozen(val)) {
return;
}
if (val.__ob__) {
const depId = val.__ob__.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);
}
}
}
// 由于这些子值这也会被遍历追踪 当这里遍历的时候 val[i] 或者 val[keys[i]] 被访问的时候(触发getter,执行 depend)
// 此时 window.target 还没有被置空。
// 因此这些子值就会将当前 Watcher 收集进去
由于受限于 Object.defineProperty()
,只有已存在的属性的变化会被追踪到,新增的属性无法被追踪到。
vm.$set
就是为了解决这个问题而出现的。
Vue.prototype.$set = function (target, key, val) {
// 如果是数组并且key是一个有效的索引值
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 取大值设置数组length属性
target.length = Math.max(target.length, key);
// 使用 splice 进行设置,因为splice会被拦截,就会侦测到target发生了变化触发watcher
// 也会将新增的val转换成响应式的。
target.splice(key, 1, val);
return val;
}
// 如果是对象且是“自己”的已经存在的key
// 是已经存在的属性,即已经是响应式的,直接修改就好
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
// 如果是对象,且是新增属性
const ob = target.__ob__;
// target 不能是Vue实例或Vue实例的根数据对象(this.$data)
// 通过 _isVue 判断是不是Vue实例
// 通过 ob.vmCount 判断是不是根数据对象
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val;
}
// 如果不是响应式对象,直接设置即可
if (!ob) {
target[key] = val;
return val;
}
// 下面就是处理:是在响应式数据上新增了一个属性
// 直接将新增属性转换为响应式数据
defineReactive(ob.value, key, val);
// 通知watcher,target发生了变化,(新增了一个属性)
ob.dep.notify();
return val;
}
与 vm.$set
一样, 是为了解决当删除一个对象属性时,Vue 无法监听到的问题。
Vue.prototype.$delete = function (target, key) {
// 如果是数组,直接使用方法进行删除,因为数组的splice方法是被代理过的
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1);
return;
}
const ob = target.__ob__;
// 也不可以在Vue实例和根数据对象上使用
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'- just set it of null.'
)
return;
}
// 如果key不是target自身的属性,则终止程序继续执行
if (!hasOwn(target, key)) {
return;
}
delete target[key];
// 如果不是一个响应式数据,则只删除,不通知,也无法进行通知。
if (!ob) {
return;
}
ob.dep.notify(); // 手动通知
}