【源码解析】解读vue2中侦听器的源码,揭秘watch的实现原理

689 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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。 今天的分享就到这里了!