Watch详解以及源码分析

693 阅读1分钟

一、侦听属性Watch

Vue 通过 watch 选项提供了一个更通用的方法,来观察和响应Vue 实例上的数据的变化。当需要在数据变化时执行异步或开销较大的操作时,使用watch最方便有效。

new Vue({
  el: '#id',
  data: {
    myName: 'zhuangguangqian'
  },
  watch: {
    myName(newName, oldName) {
      // 新值,旧值...
    }
  } 
})
  • 类型:{ [key: string]: string | Function | Object | Array }
  • 详细:watch选项是一个对象,键为我们需要观察的数据名,值为一个表达式(函数,两个形参,一个为变化后的值,一个为变化前的值),还可以是函数方法名,还可以是一个包括选项的对象。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个属性。

二、基本用法

介绍完了watch的作用和基本样子,来看下watch的用法,大致有三种:

var vm = new Vue({
  data: {
    a: 1,
    b: 2,
    c: 3,
  },
  watch: {
    //  第一种:表达式(函数)
    a: function (newval, oldVal) {
      console.log('new: %s, old: %s', newval, oldVal)
    },
    // 第二种:方法名
    b: 'watchMethod',
    // 第三种:含有选项的对象
    c: {
      handler: function (newval, oldVal) { /* ... */ },
      deep: trueimmediate: true
    }
  }
})

1. 值直接写一个监听处理函数 (逻辑较简单时)

直接令监听的数据名的值为一个监听处理表达式(函数),当每次监听到值改变时,执行该表达式(函数)。

2. 值为一个函数方法名 (逻辑较复杂时)

当监听到数据变化后的处理执行异步或者开销很大时候可以在此处令值为具体方法名,在method里在写详细的函数方法的逻辑。

3. 值为一个包括选项的对象

值是对象的时候可以包含以下三个选项: ​​

  • handler: 值是一个回调函数。即监听到变化时应该执行的函数。
  • deep:值为布尔值true或false;确认是否深度监听。(一般监听时是不能监听到对象属性值的变化的,数组的值变化可以听到。)
  • immediate:其值是true或false;确认是否以当前的初始值执行handler的函数。

重点理解: 当使用1和2方法时,就是当值第一次绑定的时候,不会执行监听函数,只有值发生改变才会执行。如果我们需要在最初绑定值的时候也执行函数,则就需要用到方法3中的immediate属性。

比如当父组件向子组件动态传值时,子组件props首次获取到父组件传来的默认值时,也需要执行函数,此时就需要将immediate设为true。

deep顾名思义,深度监听的意思。监听对象时层层遍历,并在每个对象的属性上添加侦听器,会损耗性能。后面源码分析里详解。数组(一维、多维)的变化不需要通过深度监听,对象中的属性变化则需要deep深度监听。

特别注意

  • ES6中推出了箭头函数,上面例子我均未使用箭头函数,如果在handler函数中使用箭头函数,改变了this指向,就无法获取到Vue实例,则为undifined。(踩坑)

  • 对于父子组件传参,异步获取数据有时会存在获取不到值的情况。这时候watch就派上用场,适当的时候要配合immediate或者deep属性配合使用。

三、源码分析(底层如何工作的?)

3.1 底层代码执行

源码路径:src/core/instance/state.js

  1. 初始化状态:
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {   //如果传入了watch 且 watch不等于nativeWatch(细节处理,在Firefox浏览器下Object的原型上含有一个watch函数) 坑
    initWatch(vm, opts.watch)   //初始化watch
  }
}
  1. 初始化watch
function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {   //遍历watch内多个监听内容
    const handler = watch[key]   //监听内容的值
    if (Array.isArray(handler)) {   //数组的话,遍历使用createWatcher处理
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {                //不是数组直接调用createWatcher
      createWatcher(vm, key, handler)
    }
  }
}
  1. 调用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]    // 获取methods内的方法
  }
  return vm.$watch(expOrFn, handler, options)    //最终都会调用vm.$watch方法
}
  1. $watch方法
Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,      //回调
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true               //设置options.user为true,表示这是一个用户watch
    const watcher = new Watcher(vm, expOrFn, cb, options)    //每个watch 配发一个Watcher
    if (options.immediate) {            //设置了immediate立即执行一次回调,传入监听值
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {          //注销
      watcher.teardown()
    }
  }

注意: 为什么要注销 watch?因为我们的组件是经常要被销毁的,比如我们跳一个路由,从一个页面跳到另外一个页面,那么原来的页面的 watch 其实就没用了,这时候我们应该注销掉原来页面的 watch 的,不然的话可能会导致内存溢出。好在我们平时 watch 都是写在组件的选项中的,他会随着组件的销毁而销毁。

3.2 Watcher类

源码路径:src/core/observer/watcher.js

01.png 配置项deep传入了Watcher类

get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)        //深度监听
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

3.2 deep实现

源码路径:src/core/observer/traverse.js

import { _Set as Set, isObject } from '../util/index'
import type { SimpleSet } from '../util/index'
import VNode from '../vdom/vnode'

const seenObjects = new Set()
/**
 * Recursively traverse an object to evoke all converted
 * getters, so that every nested property inside the object
 * is collected as a "deep" dependency.
 */
export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)      //判断是不是数组
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {    //如果不是array和object或者冻结对象或者是Vnode实例
    return                                                                           //拜拜
  }
  if (val.__ob__) {  // 只有object和array才有__ob__属性
    const depId = val.__ob__.dep.id  // 手动依赖收集器的id
    if (seen.has(depId)) { // 已经有收集过
      return             //拜拜
    }
    seen.add(depId)         //没有收集依赖 添加依赖   Set集合添加对象
  }
  if (isA) {            //数组
    i = val.length
    while (i--) _traverse(val[i], seen)       // 递归触发每一项的get进行依赖收集
  } else {
    keys = Object.keys(val)   //对象
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)  //递归触发子属性的get进行依赖收集
  }
}

如果this.deep == true,则触发每个深层对象的依赖,追踪其变化。traverse方法递归每一个对象或者数组,触发它们的getter,使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系。这个函数实现还有一个细节的小优化,遍历过程中会把子响应式对象通过它们的 dep.id 记录到 seenObjects,避免以后重复访问。 注意: 但是使用deep属性会给每一层都加上监听器,性能开销可能就会非常大了。这样我们可以用字符串的形式来优化:

watch: {
    'obj.a': {
      handler(val) {
       console.log('obj.a changed')
      },
      immediate: true
      deep: true
    }
  }

直到遇到'obj.a'属性,才会给该属性设置监听函数,提高性能。

四、和computed的对比

watch和computed都是以Vue的依赖追踪机制为基础的,它们都试图处理一件事情:当某一个数据(称它为依赖数据)发生变化的时候,所有依赖这个数据的“相关”数据“自动”发生变化,也就是自动调用相关的函数去实现数据的变动。

1. 相同点

它们都是以Vue的依赖追踪机制为基础的,它们的共同点是:都是希望在依赖数据发生改变的时候,被依赖的数据根据预先定义好的函数,发生“自动”的变化。

2. 不同点

2.1 处理的数据关系场景不同

03.png

watch擅长处理的场景:一个数据影响多个数据

02.png

computed擅长处理的场景:一个数据受多个数据影响

2.2 使用的偏向

watch用于观察和监听页面上的vue实例,当你需要在数据变化响应时,执行异步操作,或高性能消耗的操作,那么watch为最佳选择

可以关联多个实时计算的对象,当这些对象中的其中一个改变时都会触发这个属性 具有缓存能力,所以只有当数据再次改变时才会重新渲染,否则就会直接拿取缓存中的数据。

var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: 'Foo Bar'
  },
  watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
})
}

上面代码是命令式且重复的。将它与计算属性的版本进行比较:

var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})

更好的做法是使用计算属性而不是滥用命令式的 watch 回调

五、总结

通过以上的分析,深入理解了计算属性computed和侦听属性watch是如何工作的。计算属性本质上是一个computed watch,侦听属性本质上是一个user watch。且它们其实都是vue对监听器的实现,只不过computed主要用于对同步数据的处理,watch则主要用于观测某个值的变化去完成一段开销较大的复杂业务逻辑。。能用computed的时候优先用computed,避免了多个数据影响其中某个数据时多次调用watch的尴尬情况。

六、引发的思考

6.1 VUE的特点MVVM 数据驱动视图

怎样实现?了解内部原理,更能了解vue各个属性和功能的实现

6.2 实现一个简单的watch

怎样实现?Vue工程级框架,大项目当然使用vue。如果有个展示性的官网或者移动端H5宣传页面,完全可以不用框架。 实现一个watch类,可以让页面状态管理有条有理。getter setter实现

//公共类
function defineReactive(data, key, val, fn) {
  let subs = [] // 新增
  Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get: function() {
      // 新增
      if (data.$target) {
        subs.push(data.$target)
      }
      return val
    },
    set: function(newVal) {
      if (newVal === val) return
      fn && fn(newVal)
      // 新增
      if (subs.length) {
        // 用 setTimeout 因为此时 this.data 还没更新
        setTimeout(() => {
          subs.forEach(sub => sub())
        }, 0)
      }
      val = newVal
    },
  })
}
//watch 实现
function watch(ctx, obj) {
  Object.keys(obj).forEach(key => {
    defineReactive(ctx.data, key, ctx.data[key], function(value) {
      obj[key].call(ctx, value)
    })
  })
}

6.3 执行顺序

created computed mounted watch method

immediate为true后?