浅析watch使用方法及从内部实现解析watch的工作原理
一、watch 使用方法
1、handler方法和immediate属性
这里 watch 的一个特点是,最初绑定的时候是不会执行的,要等到 firstName 改变时才执行监听计算。那我们想要一开始就让他最初绑定的时候就执行改怎么办呢?我们需要修改一下我们的 watch 写法,修改过后的 watch 代码如下:
watch: {
firstName: {
handler(newName, oldName) {
this.fullName = newName + ' ' + this.lastName;
},
// 代表在wacth里声明了firstName这个方法之后立即先去执行handler方法
immediate: true
}
注意到handler了吗,我们给 firstName 绑定了一个handler方法,之前我们写的 watch 方法其实默认写的就是这个handler,Vue.js会去处理这个逻辑,最终编译出来其实就是这个handler。
而immediate:true代表如果在 wacth 里声明了 firstName 之后,就会立即先去执行里面的handler方法,如果为 false 就跟我们以前的效果一样,不会在绑定的时候就执行。
2、deep属性
watch 里面还有一个属性 deep,默认值是 false,代表是否深度监听。deep的意思就是深入观察,监听器会一层层的往下遍历,给对象的所有属性都加上这个监听器,但是这样性能开销就会非常大了,任何修改obj里面任何一个属性都会触发这个监听器里的 handler。
3、注销watch
为什么要注销 watch?因为我们的组件是经常要被销毁的,比如我们跳一个路由,从一个页面跳到另外一个页面,那么原来的页面的 watch 其实就没用了,这时候我们应该注销掉原来页面的 watch 的,不然的话可能会导致内置溢出。好在我们平时 watch 都是写在组件的选项中的,他会随着组件的销毁而销毁。
二、从内部实现解析watch的工作原理
watch 的几个特点:
(1)watch监听优化: 监听某个对象时,对象的任何属性改变都会触发变动, 这样比较耗性能, 如果明确知道只需监听某一属性,可以使用字符串的形式监听,如'element.data'(2)watch有一个特点,就是当值第一次绑定的时候,不会执行监听函数,只有值发生改变才会执行。如果我们需要在最初绑定值的时候也执行函数,可添加immediate属性(3)普通watch方法不能监听对象内部属性的变化,可以添加deep属性深度监听
那么watch内部是如何实现监听的呢?要知道watch的工作原理, 需要了解三个地方:
Vue会把数据设置响应式,即设置他的 get 和 set。当数据被读取,get被触发,然后收集到读取他的东西,保存到依赖收集器。当数据被改变,set被触发,然后通知曾经读取他的东西进行更新。
watch 在一开始初始化的时候,会读取一遍监听的数据的值,于是那个数据就收集到 watch 的 watcher 了 。然后你给 watch 设置的 handler ,watch 会放入 watcher 的更新函数中 。当数据改变时,通知 watch 的 watcher 进行更新,于是你设置的 handler 就被调用了。
2、设置 immediate 时,watch 如何工作的?
设置了 immediate 时,就不需要在数据改变的时候才会触发。 而是在初始化 watch 时,在读取监听数据的值之后,便立即调用一遍你设置的监听回调,然后传入刚读取的值。
3、设置了 deep 时,watch 如何工作的?
watch 有一个 deep 选项,是用来深度监听的。什么是深度监听呢?就是当你监听的属性的值是一个对象的时候,如果你没有设置深度监听,当对象内部变化时,你监听的回调是不会被触发的。
先看源码,看划线的2处,可以看到, 当存在deep属性时,会执行_traverse方法。 简单来讲,就是递归收集对象或数组的子属性值。
三、源码解读
我们常用于设置的 watch,其实它只是this.$watch
这个API的一种封装:
export default {
watch: {
name(newName) {...}
}
}
// 实际
export default {
created() {
this.$watch('name', newName => {...})
}
}
1、监听属性初始化:首先来看下初始化watch
属性时都做了什么
function initState(vm) { // 初始化所有状态时
vm._watchers = [] // 当前实例watcher集合
const opts = vm.$options // 合并后的属性
... // 其他状态初始化
if(opts.watch) { // 如果有定义watch属性
initWatch(vm, opts.watch) // 执行初始化方法
}
}
function initWatch (vm, watch) { // 初始化方法
for (const key in watch) { // 遍历watch内多个监听属性
const handler = watch[key] // 每一个监听属性的值
if (Array.isArray(handler)) { // 如果该项的值为数组
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]) // 将每一项使用watcher包装
}
} else {
createWatcher(vm, key, handler) // 不是数组直接使用watcher
}
}
}
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) // 封装
}
以上对监听属性的多种不同的使用方式,都做了处理,可以看到最后是调用了vm.$watch
方法
2、监听属性的实现原理:来看下$watch
的内部实现:
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类中
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()
}
}
export default {
data() {
return {
name: 'cc'
}
},
created() {
this.unwatch = this.$watch('name', newName => {...})
this.unwatch() // 取消监听
}
}
注意上面的 2 个地方:options.immediate 和 return unwatchFn 函数执行取消监听
虽然watch
内部是使用this.$watch
,但是我们也是可以手动调用this.$watch
来创建监听属性的,所以第二个参数cb
会出现是对象的情况。接下来设置一个标记位options.user
为true
,表明这是一个user-watcher
。再给watch
设置了immediate
属性后,会将实例化后得到的值传入回调,并立即执行一次回调函数,这也是immediate的实现原理。最后的返回值是一个方法,执行后可以取消对该监听属性的监听。
3、监听属性的deep
深度监听原理
在 Watcher 类的 get
方法内,如果有deep
属性,则执行traverse
方法
4、小结
(1)watch
和this.$watch
的实现是一致的,其原理就是为需要观察的数据创建并收集user-watcher
,当数据改变时通知到user-watcher
将新值和旧值传递给用户自己定义的回调函数
为什么watch
的回调内可以得到新值和旧值的原理,因为cb.call(this.vm, value, oldValue)
这句代码的原因,内部将新值和旧值传给了回调函数
(2)定义watch
时会被使用到的三个参数:sync
、immediate
、deep
。简单说明它们的实现原理就是:sync
是不将watcher
加入到nextTick
队列而同步的更新、immediate
是立即以得到的值执行一次回调函数、deep
是递归的对它的子值进行依赖收集。