持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第12天,点击查看活动详情
前言
大家好,上篇文章computed原理中我们分享了vue中计算属性的使用方法,特点,场景以及原理。今天再来学习一下vue中跟computed很像的另一个API - watch侦听器。相信大家对这个api应该也不陌生,侦听器相对来说用的也是比较频繁的一个api了。接下来我们还是从基本用法,使用场景和实现原理进行分析和学习。
了解侦听器(watch)
我们先来看下什么是侦听器,它是用来干嘛的,我们该如何使用它以及在什么时候使用它。
-
什么是侦听器(用途) 侦听器本质上是一个函数,一般会用来侦听某些数据的变化,然后根据这些数据的变化再做一些不同的操作(如异步操作或开销较大的操作等)
-
侦听器的用法
知道了什么是侦听器后,我们再来看看应该如何使用侦听器。侦听器主要有四种使用方式分别是:函数,对象,数组和字符串,其中函数是我们日常开发中最常使用的方式。
函数的使用方式比较简单,就是将要监听的属性直接定义为一个函数即可,但是需要注意的是不要
使用箭头函数,因为箭头函数绑定了父级作用域的上下文,所以 this
将不会按照期望指向 Vue 实例
watch:{
name: function(newVal,oldVal){
console.log('name属性发生了变化')
}
}
对象在使用对象侦听器时,对象中必须要包含一个handler
的函数,另外还可以添加一个deep或immediate属性,deep用于指示该回调在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深;immediate用于指示该回调在侦听开始之后立即调用
watch:{
name:{
handler:function(){xxx},
deep:true,
immediate:true,
}
}
数组如果是一个数组侦听器的话,数组中的值必须是函数,或者以函数名命名的字符串
watch:{
name:['someMethod', function(){}]
}
字符串字符串侦听器中的字符串不能是随意的字符串,而是已经存在的methods中方法名
watch:{
name:"handleName"
},
methods:{
handleName(){
}
}
- 侦听器使用场景 前面提到侦听器主要用于侦听一些数据的变化,然后根据这些变化再做一些额外的异步操作或开销较大的操作等。比如根据用户名获取用户的基本信息,就可以侦听用户的name属性,当name值发生变化时再去异步重新读取用户的基本信息
源码解读
initWatch
function initWatch(vm: Component, watch: Object){
for(const key in watch){
const handler = watch[key]
if(Array.isArray(handler)){
for(let i = 0; i < handler.length; i++){
createWatcher(vm, key, handler[i])
}
}else{
createWatcher(vm, key, handler)
}
}
}
- 首先定位到
src/core/instance/state.js
的293行,从initWatch开始。该方法的核心就做了一个件事:调用createWatcher方法 - initWatch接收两个参数:vm(就是Vue的实例)和对象类型的watch,该对象中包含了我们在组件中定义的所有的侦听器
- 在函数体内遍历watch对象,并取出对象中的每个侦听器保存在handler中
- 接下来判断handler是否是一个数组(因为我们定义的侦听器也可以是一个数组),如果是数组就遍历数组的每一项,并为每一项都调用createWatcher方法
- 如果不是数组就直接调用createWatcher方法
createWatcher
function createWatcher(
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
){
if(isPlainObject(handler)){
options = handler
handler = handler.handler
}
if(typeof handler === 'string'){
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
- createWatcher函数接收4个参数:vm Vue的实例,expOrFn 定义的侦听器名字, handler 侦听器的具体定义(如函数体,对象等), options一些额外的配置项(可选)
- createWatcher函数也很简单,其核心就是调用Vue实例上的$watch方法,但是在调用该方法前需要做一些特殊处理,因为在前面我们已经说过了,侦听器有四种定义方式。
- 在函数体内首先检测handler是否是一个纯对象,如果handler(定义的侦听器)是一个对象,则先将handler保存在options中,然后再将handler对象中的handler函数重新赋值给handler(因为如果定义的是一个对象侦听器,那么这个对象中必须要存在一个handler函数)
- 如果定义的侦听器是一个字符串,则需要到Vue的实例vm中找到对应字符串对应的方法(因为侦听器可以是一个字符串,但字符串必须是methods中定义的方法名)
- 最后调用Vue实例中中$watch方法
vm.$watch
经过前面一些列的处理,最终来到了vm.$watch,还是定位到src/core/instance/state.js
的348行,可以看到该方法是直接挂在Vue的原型prototype上的。
Vue.prototype.$watch = function (
expOrFn: string | (() => any),
cb: any,
options?: Record<string, any>
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn() {
watcher.teardown()
}
}
- 该方法接收三个参数:expOrFn 定义的侦听器的名字,cb 侦听器的具体定义(如函数对象等),options 额外的配置对象
- 首先将this保存在变量vm中,这里的this指向的就是Vue的实例
- 检测cb是否是一个纯对象,如果是对象继续调用createWatcher
- 关于options.user = true暂时还没弄清这句代码的意义,个人猜测可能是用来区分
是用户的watch行为还是Vue系统的watch行为的吧
- 接着下面的一句代码又出现了,看看是不是很熟悉,因为我们在上次分享的computed中刚刚分析过。也就是说除了计算属性用到了Watcher,而我们今天分享的侦听器也用到了Watcher(创建了Watcher实例),进而也就是说侦听器也是通过Watcher来管理的
- 检测options的immediate是否为true,如果为true则会调用invokeWithErrorHanling方法,而在方法内部实际是让我们自定义的侦听器函数执行了,也就是说如果immediate属性为true则会立即执行侦听器函数。
- 最后返回一个函数unwatchFn,在该函数中调用Watcher实例中的teardown方法,用于移除watcher
watcher.update
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
当所依赖的属性发生变化时就会触发watcher的update函数,在update函数中分为3个分支:
- 如果lazy属性为true则将dirty设置为true,这个dirty属性我们在computed中已经见过了,这个分支主要就是针对计算属性设置的
- 如果sync为true则调用watcher中的run函数,这个分支则是针对侦听器设置的
- 最后如果两个都不是,则调用queueWatcher函数将watcher本身添加到Watcher队列中
watcher.run
run() {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue],this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
再来看watcher中run函数
- 在run函数中,首先还是调用watcher的get函数,用于获取侦听器所依赖的属性的值
- 如果get函数获得的值与原来watcher中的值不一样了,说明依赖属性发生了变化,那么就将原来的值保存在oldValue中,然后将新值重新赋值给watcher实例的value
- 最后就是让我们自定义的侦听器函数执行,并将新值和老值都传递过去
-
在这里有个if判断,如果this.user属性为true则调用invokeWithErrorHandling方法
-
否则直接调用this.cb.call让cb执行
-
但无论走哪个分支最终目的都是让cb函数执行,也就是我们自定义的侦听器函数执行
-
总结
本文我们分享了侦听器的用法场景及其实现原理,简单总结就是:给被侦听的那个属性添加一个watcher实例,当这个属性更新的时候就会触发该watcher的update函数,update执行的时候又会触发watcher的run方法,run方法执行就会让对应的handler(我们自定义的侦听器函数)执行并且把新值和老值都传递给handler
。
今天的分享就到这里了!