浅谈Vue2选项--watch

302 阅读3分钟

侦听器的概念

  • watch 侦听器允许开发者监视数据的变化,从而针对数据的变化做特定的操作
  • 适用场景: 当需要在数据变化时执行异步开销较大的操作
  • 本质: 对象( 键是需要侦听的数据,值是对应回调函数/方法名/对象/数组
  • 使用: 定义到 watch 节点下
  • 内置参数: 新值、旧值

值为函数

 <input type="text" v-model="username">
 data(){
   return { username: 'zhangsan' }
 },
 ​
 watch: {
   username(newVal, oldVal) {
     console.log('旧值:', oldVal);
     console.log('新值:', newVal);
   }
 }

1666543132740.png

值为方法名

 watch: {
   username:'resize'
 },
 ​
 methods: {  
   resize(newVal,oldVal){
     console.log(newVal,oldVal);
   }
 }

1667027111304.png

值为对象

 watch: {
   username:{
     handler:'resize',
     immediate:true // 立即触发选项
   }
 },

1667027181520.png

值为数组

 watch: {
   username:[
     //第一个回调
     'resize', 
     // 第二个回调
     function(){
       console.log('第二个回调');
     },
     // 第三个回调
     {
       handler(){
         console.log('第三个回调');
       }
     }
   ]
 },
   
 methods: {  
   resize(newVal,oldVal){
     console.log('第一个回调');
   }
 }

1667027466533.png

键为数据名

 <input type="text" v-model="user.name">
 data(){
   return { 
     user:{ name: 'zhangsan' } 
   }
 },
 ​
 watch: {
   'user.name':function(newVal,oldVal){
     console.log('新值',newVal);
     console.log('旧值',oldVal);
   }
 },

1667027681056.png

异步操作

  • 使用 watch 选项允许执行异步操作(访问一个 API),限制执行该操作的频率,并在我们得到最终结果前,设置中间状态
 <h1>
   Ask a yes/no question:
   <input v-model="question">
 </h1>
 <h1>{{ answer }}</h1>
 import axios from 'axios'
 import _ from 'loadsh'
 ​
 export default {
   data(){
     return {
       question: '',
       answer: 'I cannot give you an answer until you ask a question!'
     }
   },
   
   created() {
     this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
   },
   
   watch: {
     //  question发生改变,函数执行
     question(newQuestion, oldQuestion) {
       this.answer = 'Waiting for you to stop typing...'
       // 添加防抖,AJAX 请求直到用户输入完毕才会发出
       this.debouncedGetAnswer()
     }
   },
   
   methods: {
     getAnswer() {
       if (this.question.indexOf('?') === -1) {
         this.answer = 'Questions usually contain a question mark. ;-)'
         return
       }
       this.answer = 'Thinking...'
       // 数据请求
       axios.get('https://yesno.wtf/api')
         .then((res)=> {
           this.answer = _.capitalize(res.data.answer)
         })
         .catch(function (error) {
           this.answer = 'Error! Could not reach the API. ' + error
       })
     }
   }
 };

1666543921617.png

侦听器选项

  • 一般侦听器有两个缺点:

    1. 无法在刚进页面的时自动触发
    2. 侦听对象时,对象的属性发生变化,不能触发侦听器

immediate选项

  • watch 默认是懒侦听,最初绑定时不会执行,需等到侦听的属性改变时才执行回调
  • watch 中首次绑定时,让侦听器自动触发一次
 watch: {
   username:{
     immediate:true,
     handler(newVal,oldVal){
       console.log('新值',newVal);
       console.log('旧值',oldVal);
     }
   }
 },

1667028125038.png

deep选项

  • watch 默认是浅层,嵌套属性的变化不会被侦听到
  • 若想侦听所有嵌套属性的变更,需要深层侦听器
 watch: {
   user:{
     deep:true,
     handler(newVal,oldVal){
       console.log('新值',newVal);
       console.log('旧值',oldVal);
     }
   }
 }

1667028419804.png

注意:由于侦听的是对象,相当于是侦听该对象的地址,所以获取到的新旧值都是同一个地址,这就会导致新旧值一致的问题

侦听器的工作原理

  • new Vue() 时调用 initState 方法,其中调用了 initWatch 方法
 if (opts.watch && opts.watch !== nativeWatch) {
   initWatch(vm, opts.watch);
 }

1667030502601.png

  • 实例上存在 watch 配置并且 watch 不是原生方法时才执行,因为火狐浏览器是自带watch属性的

initWatch函数

  • 作用: 遍历 watch 对象,然后拿到其每一项作为 handler
  • handler: 可以是一个数组,若是一个数组就遍历并调用 createWatcher,否则直接调用
 function initWatch (vm, watch) {
   for (var key in watch) {
     var handler = watch[key];
     if (Array.isArray(handler)) {
       for (var i = 0; i < handler.length; i++) {
         createWatcher(vm, key, handler[i]);
       }
     } else {
       createWatcher(vm, key, handler);
     }
   }
 }

createWatcher函数

  • 作用: 创建侦听器
 function createWatcher (vm, expOrFn, handler, options) {
   // vm:组件实例
   // expOrFn:侦听的数据
   // handler:侦听数据对应回调函数/方法名/对象/数组
   // isPlainObject用于判断是否为对象
   if (isPlainObject(handler)) {
     options = handler;
     handler = handler.handler;
   }
   if (typeof handler === 'string') {
     handler = vm[handler];
   }
   return vm.$watch(expOrFn, handler, options)
 }
  1. 如果对应的回调是对象,则将对象中的 handler函数赋给形参handler
  2. 如果对应回调是字符串,则直接调用实例上的方法
  3. 返回调用 $watch

$watch函数

 Vue.prototype.$watch = function (expOrFn, cb, options) {
   var vm = this;
   // 如果对应的回调是对象,则调用createWatcher
   if (isPlainObject(cb)) return createWatcher(vm, expOrFn, cb, options)
   options = options || {};
   options.user = true;
   var watcher = new Watcher(vm, expOrFn, cb, options);
   if (options.immediate) {
     var info = "callback for immediate watcher "" + (watcher.expression) + """;
     pushTarget();
     invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
     popTarget();
   }
   return function unwatchFn () {
     watcher.teardown();
   }
 };
  1. 首先判断对应的回调若是对象,则调用 createWatcher 方法,因为 watch 方法是可以直接调用的,它可以传递一个对象,也可以传递函数

  2. 然后让将传入的配置项赋给 options ,再把属性user置为true,这是 user watcher标识

  3. new 一个 Watcher 实例,其中四个参数分别为

    ①vm: 组件实例

    ②expOrFn: 监听的数据

    ③cb: 回调函数 handler

    ④options: 配置项

    Watcher 构造函数中,如果 expOrFn 是合法字符串,就返回一个函数赋到 watchgetter

1667036733370.png

  1. 返回函数的话 this.getter就是一个取值函数,最后会调用 this.get() 进行一次求值,对响应式对象取值就触发 get() 拦截,这时就会收集依赖 user watcher
  2. 通过实例化 Watcher,一旦侦听器的数据发生变化,最终会执行 Watcherrun 方法,执行回调

1667037475784.png

  1. 如设置了 immediate 选项,同样执行 invokeWithErrorHandling 方法,唯一不同的是调用回调之前执行了 pushTarget(),回调之后又调用 popTarget(),这种做法其实是防止收集依赖
  2. 最后返回 unwatchFn 方法,目的主要是移除侦听器

总结

  • 通过分析,大概了解到侦听器是如何工作的。
  • 侦听器本质上是一个 user watch
  • 侦听器主要用于观测数据变化去完成开销较大的复杂业务逻辑
  • 若想深入了解,还需要挖掘源码