# watch原理

watch的原理,内部是通过$watch实现的。

# watch选项初始化

在Vue初始化时,首先进行的就是数据选项的初始化,包括data、computed、以及watch,如下图所示:

function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // ...code 
  // 用户自定义watcher
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
1
2
3
4
5
6
7
8
9

initWatch就是对用户定义的watch选项进行初始化,过程如下:

  1. 循环watch选项,给每个key通过createWatcher函数,添加用户watcher,而createWatcher函数最终调用的$watch方法。
    1. 注意,watch选项有很多写法,所以在initWatch -> createWatcher方法中,分别对这些情况做了兼容处理。
function initWatch(vm: Component, watch: Object) {
  // 给每一个key创建watcher
  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)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // watcher的key是对象的写法: key: { handler: fn, deep: true, immediate: true }
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // handler是字符串的写法: key:'someMethod'
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  // 调用$watch方法
  return vm.$watch(expOrFn, handler, options)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# $watch过程分析

接下来我们详细看下$watch方法中到底做了哪些事情?

  1. 创建一个Watcher实例,我们称之为用户watcher,以options选项中的options.user = true为标识,并返回一个函数(该函数可以取消监控)。
Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      // 用户直接调用,例如vm.$watch('name',{ handler: fn, deep: true, immediate: true })
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    // 用户watcher
    options.user = true
    // 用户watcher的"expOrFn",就是是watch选项的key
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  1. 在执行new Watcher时,**expOrFn**就是我们监控的那个key_。_由于_expOrFn_是一个字符串,先通过parsePath (opens new window)函数将expOrFn转换为一个函数并赋值给this.getter,然后调用this.get方法。
    1. pushTarget:当前用户watcher入栈,放入watcher队列中
    2. 执行getter函数,对监控的key取值。由于key是响应式的,此。时会触发key对应的get (opens new window)方法,进行依赖收集。即key对应的dep会收集当前watcher,并存入subs数组中。同时,watcher也会收集当前dep,存入deps数组中。
    3. 如果deep为true,则会通过traverse(value) (opens new window)方法,循环读取value的每一个key,递归收集依赖。
    4. popTarget,依赖收集完成后,当前用户watcher出栈。
get () {
    // 全局的Dep.target = 当前渲染watcher, 或者用户watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 调用getter方法 --> updateComponent -> vm._render,vm取值,触发defineReactive的get方法,进行依赖收集。
      // 此时全局的Dep.target = this,即当前的watcher
      // value是老的值,
      // 1、对渲染watcher而言,this.getter执行时,就会执行updateComponent方法-> vm._update(vm._render()),取值并生成vnode。此时每个key的dep就会收集“渲染watcher这个依赖”
      // 2、对用户watcher而言,this.getter执行时,会去vm上取对应watch的那个key的值,然后这个key的dep就会收集"用户wather"这个依赖。
      // 3、对计算属性wather而言,this.getter,会去执行key对应的函数(这个函数内部会去取vm上的其他key值,其他key的dep就会收集计算属性watcher这个依赖)
      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
      // 如果deep为true,表示当前为用户watcher,此时的this.getter就是我们求表达式值的方法(parsePath(expOrFn)),value就是我们watch的那个key的值.
      // 深度监听时,调用traverse方法,它会遍历value对象的每一个key并取值,触发其get方法,进行依赖收集,
      // 即:将对象的每一个key对应的dep添加到当前watcher的dep数组中,同时每一个dep的subs中添加当前watcher
      if (this.deep) {
        traverse(value)
      }
      // 结束后进行清理操作
      popTarget()
      // 清理deps,将newDeps赋值给deps,newDepIds赋值给depIds。同时清空newDeps、deps
      this.cleanupDeps()
    }
    return value
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

当组件挂载时,即mountCompont (opens new window)时,会创建一个渲染watcher。这个渲染watcher会调用vm._render()方法生成vnode,再调用vm._update(vnode),进行虚拟dom的patch,最终生成真实dom。

  1. 在调用**_render**方法时,会执行组件内部的render函数(模板最终会被编译成render函数),此时会从vm实例中取data中对应key的值,触发key的get (opens new window)方法,进行依赖收集。此时key对应的dep会收集当前渲染watcher,渲染watcher也会收集key对应的这个dep。
updateComponent = () => {
  // vm._render:src/core/instance/render.js,75行,
  // vm._render生成vnode:内部调用createElement生成vnode

  // vm._update:更新vnode,位置:lifecycle.js:Vue.prototype._update
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 此时key对应的dep,就有了2个watcher依赖:[用户watcher、渲染watcher]


当key的值发生变化时,触发key的set (opens new window)方法,然后代用dep.notify通知更新。

  1. dep.notify方法,会依次循环dep的subs数组(watcher数组),调用watcher的update方法
  notify () {  // 发布
    // 获取dep对应的watcher数组
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      // watcher按照从小到大的循序排列(watcher从父到子排列)
      subs.sort((a, b) => a.id - b.id)
    }
    // 循环调用watcher.update方法
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  1. 调用update方法时,如果dirty为true(计算属性watcher),则将dirty设置为true。否则调用queueWatcher方法,将watcher放入一个队列中,并在下一个tick中依次执行watcher。
  update () {
    /* istanbul ignore else */
    if (this.lazy) {  
      // update时,如果是计算属性watcher,设置watcher的dirty为true, 稍后渲染的时候,重新取值
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
1
2
3
4
5
6
7
8
9
10
11
  1. queueWatcher方法中,依次将watcher放入queue队列中,并在nextTick中执行flushSchedulerQueue,flushSchedulerQueue会依次循环执行watcher的run方法,而watcher的run方法中,会依次执行各自watcher的get方法,进行数据的重新渲染。
function queueWatcher (watcher: Watcher) {
  // 不同dep可能依赖相同watcher,此时需要对watcher去重
  const id = watcher.id
  // 1.判断watcher是否已经存放了
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      // 2.将watcher存放到队列中
      queue.push(watcher)
    } 
    // queue the flush
    if (!waiting) {
      waiting = true
      // 3. 在下一个事件环中,调用flushSchedulerQueue,清空queue队列
      nextTick(flushSchedulerQueue)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  1. 当执行用户watcher的run方法时,由于this.user为true,如果value发生变化,则会执行对应的cb方法。
  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) {
          try {
            // 当用户watch的值发生变化,执行cb方法
            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)
        }
      }
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# $watch依赖收集流程图

# App.vue
<template>
  <div>
      <p>msg</p>
  </div>
</template>

<script>
export default {
    data() {
        return {
            msg: 'hello'
        }
    },
    watch: {
        'msg': function(newValue, oldValue) {
            
        }
    }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  1. 执行initWatch(vm.$options),初始化watch选项;
  2. 调用createWatcher,根据key、handler,对watch选项格式化;
  3. 调用$watch方法,创建用户watcher,并赋值this.getter;
 this.getter = parsePath(expOrFn)
1
  1. 调用watcher的get方法:
    1. 将当前用户watcher入栈
    2. 执行getter,对msg取值。触发msg的get方法,此时dep_msg会收集当前watcher依赖,当前watcher也会收集dep_msg。
    3. 如果deep为true,执行traverse(value) (opens new window),循环对value对象的每个key取值,递归收集依赖。
    4. 用户watcher出栈

image.png
App.vue挂载时,创建渲染watcher,执行vm._render方法,生成vnode。在这个过程中,页面会对msg取值,触发msg的get方法,此时dep_msg会收集渲染watcher依赖,渲染watcher也会收集dep_msg。至此,组件挂载完成后,dep_msg的subs数组中收集了2个依赖:[用户watcher、渲染watcher]。
image.png

当msg发生变化时:依次执行dep.notify -> 循环执行watcher.update -> queueWatcher -> flushSchedulerQueue -> watcher.run -> this.get 渲染页面。同时,如果数据变化,执行this.cb(用户的回调)。
image.png

# deep的实现原理

在执行$watch -> new Watcher时,如果deep为true,会执行**traverse(value)**方法,对value对象的每个key进行依赖收集(收集当前watcher)。因此,只要value中任何一个key的值发生变化,就会触发其响应式的set方法,对应的dep就会执行notify,通知页面更新。

// watcher get方法
get () {
    // 当前watcher入栈
    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 {
      // 如果deep为true,表示当前为用户watcher,此时的this.getter就是我们求表达式值的方法(parsePath(expOrFn)),value就是我们watch的那个key的值.
      // 深度监听时(deep为true),调用traverse方法,它会遍历value对象的每一个key并取值,触发其get方法,进行依赖收集,
      // 即:将对象的每一个key对应的dep添加到当前watcher的dep数组中,同时每一个dep的subs中添加当前watcher
      if (this.deep) {
        traverse(value)
      }
      // watcher出栈
      popTarget()
    }
    return value
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25