监听器

10/19/2021 vue

监听器的初始化也是在initState方法中,在计算属性的初始化之后,在src/core/instance/state.js文件中可查看

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
	
  // ......

  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
1
2
3
4
5
6
7
8
9
10

初始化监听器会调用initWatch方法,和initState方法定义在同一文件中

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)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

参数合并 (opens new window)小节对watch的合并做了分析,如果存在parentVal的情况下,合并后watch中的每项都会是一个数组,相同名称的监听器会合并为一个数组,所以在initWatch方法内会有一个数组的判断,接着会调用createWatcher方法

在分析之前,可借助如下例子逐步带入分析

const vm = new Vue({
	el: '#app',
	template: `<div id="app"></div>`,
	data: {
		age: 30,
		name: 'zhangsan',
		detailInfo: {
			addr: '四川省巴中市',
			phone: '7799139'
		}
	},
	watch: {
		age (val, oldVal) {
			console.log(val, oldVal)
		},
		detailInfo: {
			handler (val, oldVal) {
				console.log(val, oldVal)
			},
			deep: true
		}
	}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

createWatcherinitWatch方法在同一文件中定义

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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

createWatcher方法内部会对handler做对象和字符串的判断以便获取真正的监听器函数,需要注意的是handler如果为字符串,则会将methods中的同名方法作为监听器的handler,最后会调用vm.$watch方法

vm.$watchstateMixin方法内部定义,该方法和initState方法定义在同一文件中

export function stateMixin (Vue: Class<Component>) {
  // flow somehow has problems with directly declared definition object
  // when using Object.defineProperty, so we have to procedurally build up
  // the object here.

  // ......

  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
    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()
    }
  }
}
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

vm.$watch方法逻辑较简单,在方法内部对cb也做了一层对象判断,因为$watch方法可以直接调用,参数cb可以是对象也可以是函数,options是监听器选项,且默认存在user选项,在方法内部为每个监听器创建一个监听器实例,需要注意的是expOrFn可以是一个函数也可以是一个字符串,例如:

vm.$watch(
	function () { return this.name },
	function (val, oldVal) {
		console.log(val, oldVal)
	}
)
1
2
3
4
5
6

继续回到Watcher部分

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // 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()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  // ......

}
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

在实例化监听器watcher的过程中,会对expOrFn做类型判断,如果为函数类型则直接赋值给this.getter,如果为字符串类型,则会调用parsePath方法,该方法会返回一个函数,该方法定义在src/core/util/lang.js

export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14

接着会调用this.get方法

export default class 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
  }
	
  // ......
	
}
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

会将当前监听器watcher赋值给Dep.target,执行this.getter方法将监听数据的值赋给this.value,收集监听器watcher的依赖,接着就是判断该监听器watcher是否有深度监听选项(this.deep),如果有深度监听则会调用traverse方法,该方法在一个单独的文件中定义src/core/observer/traverse.js

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) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  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)
  }
}
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

看过Vue文档的同学都会知道,当监听一个对象,并且修改监听对象的某个键对应的值的时候,如果希望能够被监听器捕获到,这时需要深度监听,traverse方法在内部调用_traverse,在该方法内部会对被监听的数据做一层判断,如果被监听的数据val是一些简单类型的值,或者是被冻结的对象,或者为vnode会直接return,如果被监听的数据val为数组或对象的时候,会循环每一项的值并递归调用_traverse方法,这样便会触发被监听数据的每一项的Getter方法,进而实使得该监听器watcher对遍历的每一项进行依赖收集,如果不深度监听,监听器watcher只会依赖被监听的数据

_traverse方法内部有个小的优化点值得关注下,在调用该方法之前有一个对监听数据的depId收集的过程,做了去重处理,这样可以避免重复依赖 例如:

const Person = { name: '张三' }
new Vue({
	data: {
		a: {
			b: Person,
			c: Person
		}
	},
	watch: {
		a: {
			deep: true,
			handler: function (val, newVal) {
				console.log(val, newVal)
			}
		}
	}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在上面例子中,对this.a进行深度监听,在递归调用_tarverse方法时会进行depId的收集,由于this.a.bthis.a.c都是引用的同一对象,所以不会重复收集,第二次调用会直接return

继续回到get方法的逻辑,依赖收集结束后,接着会将Dep.target重置为上一状态、依赖清除

当监听器watcher依赖的数据发生变化时,会循环调用依赖数据订阅的每个watcherupdate方法

export default class Watcher {
	
  // ......
	
	update () {
		/* istanbul ignore else */
		if (this.lazy) {
			this.dirty = true
		} else if (this.sync) {
			this.run()
		} else {
			queueWatcher(this)
		}
	}

  // ......

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

当定义的监听器为同步监听器时,会直接调用this.run方法,例如:

new Vue({
	data: {
		name: '张三'
	},
	watch: {
		name: {
			sync: true,
			handler: function (val, newVal) {
				console.log(val, newVal)
			}
		}
	}
})
1
2
3
4
5
6
7
8
9
10
11
12
13

上述例子就是同步监听器,一般默认的监听器都是异步的,则会调用queueWatcher方法,异步更新,最后还是会执行this.run方法,queueWatcher方法在派发更新小节已经讲解过,这里就不多说明,直接看this.run方法的定义

export default class Watcher {
	
  // ......
	
  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)
        }
      }
    }
  }
	
  // ......

}
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

派发更新小节也有对this.run方法的说明,该方法会在内部执行this.get方法,获取被监听数据变化后的值,如果变化前的值和变化后的值不相同或者被监听的数据为一个对象,或者为深度监听,则将新旧值做一个交换,因为监听器watcher默认会存在user选项,所以会进入true分支执行监听器函数this.cbvalueoldValue会分别为调用监听器函数this.cb的第一个和第二个参数

至此,监听器部分则全部分析完毕

最后更新时间: 12/4/2022, 1:44:46 PM