系列文章
- [Vue源码学习] new Vue()
- [Vue源码学习] 配置合并
- [Vue源码学习] $mount挂载
- [Vue源码学习] _render(上)
- [Vue源码学习] _render(下)
- [Vue源码学习] _update(上)
- [Vue源码学习] _update(中)
- [Vue源码学习] _update(下)
- [Vue源码学习] 响应式原理(上)
- [Vue源码学习] 响应式原理(中)
- [Vue源码学习] 响应式原理(下)
- [Vue源码学习] props
- [Vue源码学习] computed
- [Vue源码学习] watch
- [Vue源码学习] 插槽(上)
- [Vue源码学习] 插槽(下)
前言
Vue通过watch选项,提供了一种更通用的方式来观察和响应Vue实例上的数据变动,那么接下来,就来看看在Vue中,是如何使用watch选项的。
watch
在初始化Vue实例的过程中,如果检测到配置中存在watch选项,就会调用initWatch方法,处理侦听属性,代码如下所示:
/* 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)
}
}
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)
}
}
}
可以看到,在initWatch方法中,就是遍历watch选项,继续调用createWatcher方法,做进一步的处理,代码如下所示:
/* core/instance/state.js */
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)
}
可以看到,createWatcher方法就是用来规范化传入的参数,在得到handler处理函数后,就去调用$watch方法,所以在watch选项中定义的侦听属性,与手动调用$watch方法的作用是相同的。
$watch
$watch方法是在引入Vue时,添加到Vue的原型上的,代码如下所示:
/* core/instance/state.js */
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) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn() {
watcher.teardown()
}
}
可以看到,在$watch方法中,首先在options上添加user属性,表明这是一个user watcher,然后创建Watcher实例,与之前章节中创建Watcher实例类似,但是对于自定义Watcher来说,它会传入一个在数据改变时调用的回调函数,还可以传入自定义配置选项,比如deep、sync等,并且以前的渲染Watcher和计算Watcher,它们的expOrFn参数都是一个函数,而自定义Watcher除了传入函数以外,还可以是一个字符串,它会经过parsePath方法处理后,再赋值给getter属性,代码如下所示:
/* core/observer/watcher.js */
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
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
)
}
}
// ...
}
}
/* 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('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
可以看到,watcher.getter指向了parsePath返回的匿名函数。回到Watcher的构造函数中,如果此时lazy为false,那么就会继续调用get方法,进行依赖收集,这时,就会调用getter方法,也就是parsePath中的匿名函数,此时,匿名函数的参数obj就是当前Vue实例,segments就是通过.分割字符串后得到的片段数组,然后遍历此数组,从而获取最终的值,从这里也可以看出,对于当前还不存在的数据,进行依赖收集时是不会报错的。
执行完watcher.get方法后,将待侦听的属性对应的值保存在watcher.value中,并且此表达式与自定义Watcher之间,已经成功构建了联系。回到$watch方法中,最后会返回一个unwatchFn方法,用来取消观察,调用此方法时,会调用watcher.teardown方法,代码如下所示:
/* core/observer/watcher.js */
export default class Watcher {
teardown() {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
可以看到,在teardown方法中,首先对此Watcher所依赖的数据,通过调用dep.removeSub方法进行取消依赖,然后将此Watcher的active选项置为false,从而取消观察,这样一来,当侦听的属性发生变化时,就不会触发该Watcher的更新了。
接下来,就来看看数据发生变化时,Vue是如何处理的。
update
当观察的数据发生变化时,就会通知此Watcher调用它的update方法进行更新,对于非sync的Watcher来说,同样也会将Watcher实例推入queue中,然后在下一帧中执行watcher.run方法,代码如下所示:
/* core/observer/watcher.js */
export default class Watcher {
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
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 {
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)
}
}
}
}
}
可以看到,在watcher.run方法中,首先还是会调用watcher.get方法,重新收集依赖,得到更新之后的值,如果和上一次的value不相同时,就会调用watcher.cb方法,也就是调用$watch时传入的handle处理函数,同时将新旧value作为参数传入,所以我们定义的回调函数才可以执行。这就是自定义Watcher更新时的逻辑。
除了默认操作外,自定义Watcher还可以接收一些额外的选项,那么接下来,就来看看这些选项是如何运作的。
immediate
在调用$watch创建自定义Watcher后,如果immediate选项为true,就会立即调用一次cb回调函数,代码如下所示:
/* core/instance/state.js */
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
// ...
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}"`)
}
}
// ...
}
deep
在调用watcher.get进行依赖收集时,如果deep选项为true,就会调用traverse方法,进行深度依赖,代码如下所示:
/* core/observer/watcher.js */
export default class Watcher {
get() {
// ...
if (this.deep) {
traverse(value)
}
// ...
}
}
/* 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__) {
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)
}
}
在调用traverse方法时,此时的Dep.target还是指向当前的Watcher实例,然后调用_traverse方法,深度递归的对碰触到的每个数据都进行依赖收集,并且还使用seenObjects避免循环依赖,最终,此Watcher会依赖所有碰触到的数据,任意层次的数据发生变化时,都会通知此Watcher进行更新。
sync
在Watcher需要进行更新时,如果sync选项为true,不会将此Watcher添加到queue中,而是直接执行watcher.run方法,进行更新操作,代码如下所示:
export default class Watcher {
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
总结
在Vue中,可以使用watch选项或$watch方法,对属性进行侦听,当这些数据发生变化时,会执行我们传入的回调函数,并且自定义Watcher还可以支持多种配置选项,从而实现不同的功能。