持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情
面试题:
$watch和watch监听的事件,执行几次?
当数据需要变化或开销较大的时候,vue提供了一个通用的方法$watch(watch)来响应数据的变化,下面分别介绍两个方法的实现原理和监听事件执行次数的分析。
一、$watch
new Vue({
el: '#app',
template: '<div><h3>姓名:{{person.name}},年龄:{{person.age}}</h3><button @click="change">change</button></div>',
data: {
person: {
name: '水中水',
age: 30
}
},
created() {
this.$watch(
'person',
function (newVal, oldVal) {
console.log(newVal, oldVal)
}, {
deep: true, // 针对对象或者数组的监听
immediate: false, // 是否立即执行,默认不立即执行
}
)
},
methods: {
change() {
this.person.age = this.person.age + 1;
}
},
})
在当前例子中定义了数据person,在created阶段通过this.$watch的方式监听了person,如果发生变化会进行新旧值的打印。
1、定义$watch
在 Vue 定义完成以后,在执行 stateMixin(Vue) 的时候会为 Vue.prototype 上定义 $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
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()
}
}
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)
}
这里首先判断cb是不是对象,如果是的话执行createWatcher逻辑,将hander作为参数options, 将其中的 handler.handler作为参数 handler。然后判断handler是不是字符串,如果是的话,从当前vm实例中去寻找hander,然后再返回vm.$watch(expOrFn, handler, options)。createWatcher的目的是处理非函数的cb参数,最终还是会执行$watch。
如果cb是函数的话,将options的user设置为true,通过执行const watcher = new Watcher(vm, expOrFn, cb, options)进行Watcher的实例化。
当参数immediate为true的时候,还会立即执行cb回调函数,当前例子为false,所以不会立即执行。
最后,返回执行watcher.teardown()的unwatchFn函数,也就是说,我们可以通过this.$watch的回调函数,来取消侦听的函数。
2、实例化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()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
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
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
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的过程中,需要关注传入的参数user和deep为true,传入的immediate为false。
参数expOrFn是person,类型为string,会执行转换的逻辑this.getter = parsePath(expOrFn):
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
}
}
这里返回一个回调函数,函数在不断地通过路径访问obj的过程中,会对整条路径对应的值进行watcher的收集,例如:obj.a.b会分别对obj、obj.a和obj.a.b访问时进行watcher的收集。当前例子obj为person,那么在访问person时就会对其持有的发布者dep的subs中推入当前的侦听器watcher。
在执行this.get时,会执行到this.getter中返回的回调函数,实现侦听器watcher的收集。
3、监听变化
当点击change按钮对age进行增加时,会触发watcher.run()的更新逻辑。通过value = this.get()计算新值,通过const oldValue = this.value将旧值进行暂存,将this.value赋值成新值value。
侦听器watcher的参数user为true,所以会将新旧值作为参数执行this.cb.call(this.vm, value, oldValue)的逻辑,也就是当前例子中的console.log(newVal, oldVal),当然打印逻辑可以换成我们真实的业务逻辑。
4、用$watch监听两次
以上例子在created阶段修改为:
created() {
this.$watch(
'person',
function (newVal, oldVal) {
console.log(newVal, oldVal)
}, {
deep: true, // 针对对象或者数组的监听
immediate: false, // 是否立即执行,默认不立即执行
}
)
this.$watch(
'person',
function (newVal, oldVal) {
console.log('--再次被监听--', newVal, oldVal)
}, {
deep: true, // 针对对象或者数组的监听
immediate: false, // 是否立即执行,默认不立即执行
}
)
},
当前例子中,我们执行了两次new Watcher的逻辑,每次都会执行this.get进行值的计算,计算过程中访问person时会利用发布订阅者模式进行当前watcher的收集,当前例子中会收集两次,在数据发生变化后两次监听都会执行。
二、watch
new Vue({
el: '#app',
template: '<div><h3>姓名:{{person.name}},年龄:{{person.age}}</h3><button @click="change">change</button></div>',
data() {
return {
person: {
name: '水中水',
age: 30
}
}
},
watch: {
person: {
handler(newVal, oldVal) {
console.log(newVal, oldVal)
},
deep: true, // 针对对象或者数组的监听
immediate: false, // 是否立即执行,默认不立即执行
}
},
methods: {
change() {
this.person.age = this.person.age + 1;
}
},
})
1、initWatch
在执行new Vue的过程中,执行到initState,当满足if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch); }时,执行initWatch初始化侦听器:
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)
}
}
}
watch[key]可以是数组类型,侦听器支持数据改变的时候触发多个行为,例子中走到else逻辑,执行createWatcher(vm, key, handler)。到这里就和$watch的逻辑类似,不再赘述。
2、用watch监听两次
反例一
watch: {
person: {
handler(newVal, oldVal) {
console.log(newVal, oldVal)
},
deep: true, // 针对对象或者数组的监听
immediate: false, // 是否立即执行,默认不立即执行
}
},
watch: {
person: {
handler(newVal, oldVal) {
console.log('--再次被监听--', newVal, oldVal)
},
deep: true, // 针对对象或者数组的监听
immediate: false, // 是否立即执行,默认不立即执行
}
},
反例二
watch: {
person: {
handler(newVal, oldVal) {
console.log(newVal, oldVal)
},
deep: true, // 针对对象或者数组的监听
immediate: false, // 是否立即执行,默认不立即执行
},
person: {
handler(newVal, oldVal) {
console.log('--再次被监听--', newVal, oldVal)
},
deep: true, // 针对对象或者数组的监听
immediate: false, // 是否立即执行,默认不立即执行
}
},
以上两个例子中,因为watch或者person作为对象的键,后定义的会覆盖前面定义的,所以只会执行后面定义的监听回调。如果想实现两次监听都执行,可以采用以下写法:
watch: {
person: [
{
handler(newVal, oldVal) {
console.log(newVal, oldVal)
},
deep: true, // 针对对象或者数组的监听
immediate: false, // 是否立即执行,默认不立即执行
},
{
handler(newVal, oldVal) {
console.log('--再次被监听--', newVal, oldVal)
},
deep: true, // 针对对象或者数组的监听
immediate: false, // 是否立即执行,默认不立即执行
}
]
},
以上写法可以实现数据变化时,监听的两个事件都执行。
总结
vue的watch和$watch最终都是实例化了Watcher,并设置user为true和其他watcher进行区分,都支持数据改变的时候触发多个行为。