【Vue原理】watch源码分析

879 阅读5分钟

感觉自己比较守承诺,写完【Vue原理】$nextTick源码分析,接着立马另一篇原理出世,每天上完班回来之后,晚上回来还得写到11点半,有人说都有孩子了为啥这么拼,我只知道“性格决定命运”,没有充实就没有快乐。

读完这篇的收获:

  1. Watch的使用方法
  2. Watch监听的原理
  3. 加深响应式原理的理解

相信很多人跟我一样,看到watch就想到computed,不管是面试上还是使用上,肯定要懂得他们之间的区别

哈哈,本着精华在前和孰能生巧(多看到几遍)的思想,所以正式写文章前先总结为有以下几点:

  1. 更新时依赖收集:computed更新的前提需要“渲染Watcher”的配合,因此依赖属性的 subs 中至少会存储两个 Watcher;Watcher不需要
  2. 触发时机:computed是依赖的值发生变化才会执行;watch是监听定义的属性,只要此属性发生变化,就会触发回调
  3. 缓存:computed 有缓存机制;watch没有,不管属性有没有变化都会执行
  4. 应用场景:computed一般应用于不经常变化的、可能高计算量、需要缓存的地方;watch应用于异步请求、可有中间变量(可以配合v-model等指令使用)

在此说下自己的计划下一篇是写响应式原理,其实这篇已经在我的vue双向绑定原理中提到,但是随着理解的深入,还想花时间更易让大家理解抽离出来。同时这篇文章还会更详细。

watch介绍

作用

监听属性的变化

写法

  1. 字符串声明
watch: {
  message: 'handler'
},
methods: {
  handler (newVal, oldVal) { /* ... */ }
}
  1. 函数声明
 watch: {
  dragList: {
    handler: function(newValue, oldValue) {},
    // 回调会在监听开始之后被立即调用
    immediate: true,
    // 对象深度监听  对象内任意一个属性改变都会触发回调
    deep: true
  }
}

  1. 对象声明
 watch: {
  // 使用handler,监听对象的某个属性,
  'a.b': {
    handler: function (newVal, oldVal) { /* ... */ }
  }
 }
  省去function,直接接函数
  watch: {
    'a.b.c'() {
      
    }
  }
  
  watch: {
    a() {}  可以不使用引号
  },
 直接后面接函数
  watch: {
    "$props.src": function(val) {}
  }
  1. 数组声明
// 传入回调数组,它们会被逐一调用
  watch: {
    'a.b': [
      'handle',
      function handle2 (newVal, oldVal) { /* ... */ },
      {
        handler: function handle3 (newVal, oldVal) { /* ... */ },
      }
    ],  
  },
  methods: {
    handler (newVal, oldVal) { /* ... */ }
  }
  1. vm.$watch( expOrFn, callback, [options] )
// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
  // 做点什么
})

// 函数
vm.$watch(
  function () {
    // 表达式 `this.a + this.b` 每次得出一个不同的结果时
    // 处理函数都会被调用。
    // 这就像监听一个未被定义的计算属性
    return this.a + this.b
  },
  function (newVal, oldVal) {
    // 做点什么
  }
)

deep属性

为了发现对象内部值的变化,可以在选项参数中指定 deep: true。注意监听数组的变更不需要这么做。

vm.$watch('someObject', callback, {
  deep: true
})
vm.someObject.nestedValue = 123
// callback is fired

注意:deep的意思就是深入观察,监听器会一层层的往下遍历,给对象的所有属性都加上这个监听器,但是这样性能开销就会非常大了,任何修改obj里面任何一个属性都会触发这个监听器里的 handler。

优化,我们可以是使用字符串形式监听。

watch: {
  'obj.a': {
    handler(newName, oldName) {
      console.log('obj.a changed');
    },
    immediate: true,
    // deep: true
  }
} 

复制代码这样Vue.js才会一层一层解析下去,直到遇到属性a,然后才给a设置监听函数。

immediate属性

watch 有一个特点是,最初绑定的时候是不会执行的,要等到 'a' 改变时才执行监听计算。 不过当写了immediate: true,一开始最初绑定的时候就执行callback

在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调:

vm.$watch('a', callback, {
  immediate: true
})
// 立即以 `a` 的当前值触发回调

注意在带有 immediate 选项时,你不能在第一次回调时取消侦听给定的 property。

// 这会导致报错
var unwatch = vm.$watch(
  'value',
  function () {
    doSomething()
    unwatch()
  },
  { immediate: true }
)

如果你仍然希望在回调内部调用一个取消侦听的函数,你应该先检查其函数的可用性:

var unwatch = vm.$watch(
  'value',
  function () {
    doSomething()
    if (unwatch) {
      unwatch()
    }
  },
  { immediate: true }
)

取消(注销)watch

vm.$watch 返回一个取消观察函数,用来停止触发回调:

var unwatch = vm.$watch('a', cb)
// 之后取消观察
unwatch()  //直接调用

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

watch源码分析

从入口到获取到$watch函数的执行流程

入口:

源码:github.com/vuejs/vue/b…

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

_init:

源码:https://github.com/vuejs/vue/blob/dev/src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    // a flag to avoid this being observed  一个避免被observed的标志
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      //优化内部组件实例化,因为动态选项合并非常慢,而且没有一个内部组件选项需要特殊处理。
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    // 初始化数据
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')


    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

initState:

源码:github.com/vuejs/vue/b…

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) {
  //初始化data
    initData(vm) 
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
    // 这里会初始化 watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initData:

源码:github.com/vuejs/vue/b…

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(... )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

initWatch:

源码:github.com/vuejs/vue/b…

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
    //1.数组声明的 watch 有多个回调,需要循环创建监听
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
    //2.其他声明方式直接创建
      createWatcher(vm, key, handler)
    }
  }
}

createWatcher:

源码:github.com/vuejs/vue/b…

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
// 1. 对象声明的 watch,从对象中取出对应回调
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
// 2. 字符串声明的 watch,直接取实例上的方法(注:methods 中声明的方法,可以在实例上直接获取)
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
// 3. expOrFn 是 watch 的 key 值,$watch 用于创建一个“用户Watcher”
  return vm.$watch(expOrFn, handler, options)
}

所以在创建数据监听时,除了 watch 配置外,也可以vm.$watch 方法实现同样的效果。

stateMixin:

源码:github.com/vuejs/vue/b…

export function stateMixin (Vue: Class<Component>) {
  // 在使用object.defineproperty时,流在直接声明定义对象方面存在一些问题,因此我们必须在这里逐步构建该对象。
  const dataDef = {}
  dataDef.get = function () { return this._data }
  const propsDef = {}
  propsDef.get = function () { return this._props }
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)
  Vue.prototype.$set = set
  Vue.prototype.$delete = del
  Vue.prototype.$watch = function (){
  	下面
  	...
  }
}

stateMixin 在入口文件就已经调用了,为 Vue 的原型添加 $watch 方法。

$watch

源码:github.com/vuejs/vue/b…

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
     // 对象声明的cb,直接返回createWatcher
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    // 所有“用户Watcher”的 options,都会带有 user 标识
    options.user = true
     // 创建 watcher,进行依赖收集
    const watcher = new Watcher(vm, expOrFn, cb, options)
     // immediate 为 true 时,会立即调用回调
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    // 取消 watch 监听
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}

options.deep怎么没有处理呢?

watch监听的内容依赖收集

依赖收集到底是什么啊?到处都用到它,

根据$watch,会调用new Watch(),这里是依赖收集和更新的触发点。 响应式那里还会有这块的具体逻辑

// 源码位置:/src/core/observer/watcher.js
export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    // parse expression for getter
    
    // 1 start 
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    // 1 end
    
    
    // 2 start 
    this.value = this.lazy
      ? undefined
      : this.get()
    // 2 end 
  }
}


根据vm.$watch( expOrFn, callback, [options] )中写法

// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
})

// 函数
vm.$watch(
  function () {
    return this.a + this.b
  },
  function (newVal, oldVal) {
  })

分析以上代码中的代码段1,传进来的 expOrFn 是 watch 的键值;其他是键路径,也就是例中的'a.b.c',需要调用 parsePath 对键值解析。

这一步也是依赖收集的关键点。它执行后返回的是一个函数,先不着急 parsePath 做的是什么,先接着流程继续走。

parsePath是什么呢?

源码:/src/core/util/lang.js

const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.') //根据上面举例path为'a.b.c',所以segments为键名数组
  //obj在get函数中的value = this.getter.call(vm, vm)传入,参数 obj 是 vm 实例
  return function (obj) {  
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]  //循环去获取每项键名的值,触发它们的“数据劫持get”
    }
    return obj
  }
}

疑问?? 循环去获取每项键名的值,触发它们的“数据劫持get”,每个键名都能在data属性中找到吗? 突然间明白了,假如现有obj中没有x,但是执行obj['x']也是会触发get,也就是会执行dep.depend()

根据parsePath代码,看出它是返回一函数,函数中返回监听对象中的任何一部分

代码段2,默认调用this.get()

get () {
// 将当前的“用户Watcher”(即当前实例this) 挂到 Dep.target 上,在收集依赖时,找的就是 Dep.target
  pushTarget(this)
  let value
  const vm = this.vm
  try {
  //调用this.getter函数,也就是会调用expOrFn或者parsePath
    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
}

pushTarget 将当前的“用户Watcher”(即当前实例this) 挂到 Dep.target 上,在收集依赖时,找的就是 Dep.target。然后调用 getter 函数,这里就进入 parsePath 的逻辑。

以上执行顺序是:执行vm.$watch, 执行const watcher = new Watcher(vm, expOrFn, cb, options),Watcher中会触发get(),再触发parsePath,也就是会触发“数据劫持get”,再收集依赖(再任何确实是什么?我的理解依赖就是被依赖的属性,也就是watch)

参数 obj 是 vm 实例,segments 是解析后的键值数组,循环去获取每项键值的值,触发它们的“数据劫持get”。接着触发 dep.depend 收集依赖(依赖就是挂在 Dep.target 的 Watcher)。

到这里依赖收集就完成了,从上面我们也得知,每一项键值都会被触发依赖收集,也就是说上面的任何一项键名的值发生改变都会触发 watch 回调。例如:

watch: {
    'obj.a.b.c': function(){}
}

复制代码不仅修改 c 会触发回调,修改 b、a 以及 obj 同样触发回调。这个设计也是很妙,通过简单的循环去为每一项都收集到了依赖。

get()在Watcher 中调用

watch监听更新

在更新时首先触发的是“数据劫持set”,调用 dep.notify 通知每一个 watcher 的 update 方法。

update () {
  if (this.lazy) { dirty置为true
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

接着就走 queueWatcher 进行异步更新,这里先不讲异步更新。只需要知道它最后会调用的是 run 方法。

run () {
  if (this.active) {
    const value = this.get() //获取新值,怎么获取的?
    if (
      value !== this.value ||
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

this.get 获取新值,调用 this.cb,将新值旧值传入。

总之:

watch有响应式的整个逻辑,也就是get和set,get会收集依赖,set会触发更新

深度监听(deep为true)

深度监听是 watch 监听中一项很重要的配置,它能为我们观察对象中任何一个属性的变化。

get 函数有一段代码是这样的:

if (this.deep) {
  traverse(value)
}

traverse

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

const seenObjects  = new Set()

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) {
    return
  }
  if (val.__ob__) {
    // 1 depId 是每一个被观察属性都会有的唯一标识
    const depId = val.__ob__.dep.id
    // 2 去重,防止相同属性重复执行逻辑
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  // 3 根据数组和对象使用不同的策略,最终目的是递归获取每一项属性,
  触发它们的“数据劫持get”收集依赖,和 parsePath 的效果是异曲同工
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

从这里能得出,深度监听利用递归进行监听,肯定会有性能损耗。因为每一项属性都要走一遍依赖收集流程,所以在业务中尽量避免这类操作。

取消(注销)watch

unwatchFn不常用,但是也讲下其中原理

Vue.prototype.$watch中返回unwatchFn函数

 return function unwatchFn () {
    watcher.teardown()
  }

teardown

teardown () {
  if (this.active) {
    if (!this.vm._isBeingDestroyed) {
      remove(this.vm._watchers, this)
    }
    let i = this.deps.length
    while (i--) {
      this.deps[i].removeSub(this)
    }
    this.active = false
  }
}

遍历 deps 调用 removeSub 方法,移除当前 watcher 实例。在下一次属性更新时,也不会通知 watcher 更新了。deps 存储的是属性的 dep。

总结主要流程

initWatch(vm, opts.watch)
-->

createWatcher(vm, key, handler[i]) [key代表监听的键名,数组需要循环创建监听,其他直接创建,那对象呢?] return vm.watch(expOrFn,handler,options)expOrFnwatchkey值,watch(expOrFn, handler, options) 【expOrFn 是 watch 的 key 值,watch 用于创建一个“用户Watcher”】

-->

Vue.prototype.$watch = function (){
  const watcher = new Watcher(vm, expOrFn, cb, options)
}

创建watcher收集依赖,cb是A函数,意味着立即调用回调

 if (options.immediate) {
      try {
        cb.call(vm, watcher.value}
      }
}

--> 触发update ()函数,获取新值,调用B函数,这样把新旧值都传入

watch中如果有deep,所以在get()函数中需要调用traverse

当deep为true时,watch 监听实现利用遍历获取属性,触发“数据劫持get”逐个收集依赖,这样做的好处是其上级的属性发生修改也能执行回调。

if (this.deep) {
  traverse(value)
}