跳到主要内容

watch

watch的使用

能使用watch属性的场景基本上都可以使用computed属性,而且computed属性开销小,性能高,因此能使用computed就尽量使用computed属性

但是当您想要执行异步或昂贵的操作以响应不断变化的数据

监听的数据改变的时,watch 如何工作

watch 在一开始初始化的时候,会 读取 一遍 监听的数据的值,于是,此时 那个数据就收集到 watch 的 watcher 了

然后 你给 watch 设置的 handler ,watch 会放入 watcher 的更新函数中

当 数据改变时,通知 watch 的 watcher 进行更新,于是 你设置的 handler 就被调用了

设置 immediate 时,watch 如何工作

当你设置了 immediate 时,就不需要在 数据改变的时候 才会触发。

而是在 初始化 watch 时,在读取了 监听的数据的值 之后,便立即调用一遍你设置的监听回调,然后传入刚读取的值

设置了 deep 时, watch 如何工作

我们都知道 watch 有一个 deep 选项,是用来深度监听的。

Vue在设置响应式数据的时候, 遇到值是对象的,会递归遍历,把对象内所有的属性都设置为响应式,就是每个属性都设置 get 和 set,于是每个属性都有自己的一个依赖收集器

首先,再次说明,watch初始化的时候,会先读取一遍监听数据的值

源码分析

上边提到了在new Vue()的时候调用了_init方法完成了初始化。在这当中有调用了initWatch方法,定义在src/core/instance/state.js中:

function initWatch (vm, watch) {
// 遍历watch属性
for (var key in watch) {
var handler = watch[key];
// 如果是数组,那么再遍历一次
if (Array.isArray(handler)) {
for (var i = 0; i < handler.length; i++) {
// 调用createWatcher
createWatcher(vm, key, handler[i]);
}
} else {
// 同上
createWatcher(vm, key, handler);
}
}
}

遍历watch对象,并将每个watch[key]赋值给handler,如果是数组则遍历电影createWatcher方法,否则直接调用createWatcher方法。接下来看一下createWatcher方法的定义:

function createWatcher (vm, expOrFn, handler, options) {
if (isPlainObject(handler)) { // 如果是对象,参数移位
options = handler
handler = handler.handler
}
if (typeof handler === 'string') { // 如果是字符串,表示为方法名
handler = vm[handler] // 获取methods内的方法
}
return vm.$watch(expOrFn, handler, options) // 封装
}


通过代码可以发现,createWatcher方法vm.$watch(keyOrFn, handler, options) 函数,调用了Vue.prototype.$watch方法,定义在src/core/instance/state.js中:


Vue.prototype.$watch = function(expOrFn, cb, options = {}) {
const vm = this
if (isPlainObject(cb)) { // 如果cb是对象,当手动创建监听属性时
return createWatcher(vm, expOrFn, cb, options)
}

options.user = true // user-watcher的标志位,传入Watcher类中
//这里注意 new了一个Watcher,cb是之前的handler,expOrFn是之前的key
const watcher = new Watcher(vm, expOrFn, cb, options) // 实例化user-watcher

if (options.immediate) { // 立即执行
cb.call(vm, watcher.value) // 以当前值立即执行一次回调函数
} // watcher.value为实例化后返回的值

return function unwatchFn () { // 返回一个函数,执行取消监听
watcher.teardown()
}
}
  • watch 最终会调用Vue.prototype.watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。
  • 接着执行 const watcher = new Watcher(vm, expOrFn, cb, options) 实例化了一个 watcher,这里需要注意一点这是一个 user watcher,因为 options.user = true。
  • 通过实例化 watcher 的方式,一旦我们 watch 的数据发送变化,它最终会执行 watcher 的 run 方法,执行回调函数 cb,并且如果我们设置了 immediate 为 true,则直接会执行回调函数 cb 。即设置immediate属性为true的时候,第一次watch绑定的时候就可以执行。
  • 最后返回了一个 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher。

重点是new Watcher中的处理

// 简化成一个类
class Watcher {
constructor(vm, expOrFn, cb, options) {
this.vm = vm
vm._watchers.push(this) // 添加到当前实例的watchers内

if(options) {
this.deep = !!options.deep // 是否深度监听
this.user = !!options.user // 是否是user-wathcer
this.sync = !!options.sync // 是否同步更新
}

this.active = true // // 派发更新的标志位
this.cb = cb // 回调函数

if (typeof expOrFn === 'function') { // 如果expOrFn是函数
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn) // 如果是字符串对象路径形式,返回闭包函数
}

this.value = this.lazy
? undefined
: this.get();

...

}


链式访问属性的特殊处理 data.a.b.c

如果是字符就去匹配这个可以对应的方法

const bailRE = /[^\w.$]/  // 得是对象路径形式,如info.name

function parsePath (path) {
if (bailRE.test(path)) return // 不匹配对象路径形式,再见

const segments = path.split('.') // 按照点分割为数组

return function (obj) { // 闭包返回一个函数
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]] // 依次读取到实例下对象末端的值
}
return obj
}
}

根据key提取到的这个getter方法,其实是key路径最后的那个属性的值,也就是属性变了执行的方法.

Watcher.prototype.get = function get () {     //第3135行
pushTarget(this); //将当前用户watch保存到Dep.target总=中
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm); //执行用户wathcer的getter()方法,此方法会将当前用户watcher作为订阅者订阅起来
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value);
}
popTarget(); //恢复之前的watcher
this.cleanupDeps();
}
return value
};

new Watcher 的简化版

  • 就是初始化话的时候去访问已经响应式的watch的key值
  • 然后触发响应式的依赖收集,但此时全局的Dep.target在new Watch的时候指向的是此时的user watcher
var Watcher = function (vm, key, cb, opt) {  

this.vm = vm;

this.deep = opt.deep;

this.cb = cb;

// 这里省略处理 xx.xx.xx 这种较复杂的key
this.getter = function(obj) {

return obj[key]

};

// this.get 作用就是执行 this.getter函数

this.value = this.get();
};

重中之重是 this.get() 会初始化的时候调用

在vm._init方法中,initWatch是在initData之后,initData中defineReactive(vm.data)已经为data的所有可达属性重写了set和get方法,是可以收集watcher的。

所以 Watch 在结尾会立即执行一次 watcher.get,其中便会执行 getter,便会根据你监听的key,去实例上读取并返回,存放在 watcher.value 上(触发响应式依赖收集)

每个 data 数据都已经是响应式的当 watch.getter 执行,而读取了 vm.name 的时候,name的依赖收集器就会收集到 watch-watcher

更新

get方法闭包中的dep收集这个新new出来的watcher,那么以后vm.name的set方法被调用的时候,就会通过dep.notify调用所收集的watcher的update方法,从而调用run方法,进而调用cb方法。 于是 name 变化的时候,会可以通知到 watch,监听就成功了

Watcher.prototype.update = function update () {
/* istanbul ignore else */
console.error('watch-update!!!');
if (this.lazy) {
//computed属性的watcher的lazy为true
console.log('lazy...');
this.dirty = true;
} else if (this.sync) {
// //如果this.sync为true,则直接运行this.run获取结果
console.log('sync...');
this.run();
} else {
////this.sync为false,调用queueWatcher()函数把所有要执行update()的watch push到队列中
console.log('other...');
queueWatcher(this);
}
};


Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get();
if (
value !== this.value ||
//对于引用类型,值相同,可能指向的内存地址不同
isObject(value) ||
this.deep
) {
// set new value
var oldValue = this.value;
//其实执行的是 this.value=this.get();
this.value = value;
this.cb.call(this.vm, value, oldValue)
}
}
};