watch 是 vue 提供的侦听器,用于监听属性的变化;
Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的;
来自 官方文档
<template>
<div>
<button @click="add">点击</button>
</div>
</template>
<script>
export default {
data() {
return {
i: 0
};
},
watch: {
i(newVal, oldVal) {
console.log(newVal, oldVal);
}
},
methods: {
add() {
this.i++;
}
}
}
</script>
上面的例子,使用 watch 监听 data.i 的变化:当 data.i 被修改时,则会触发 watch 的回调函数。
还有许多种用法,例子来自官方文档:
watch: {
// 函数
a: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
// 方法名
b: 'someMethod',
// 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
c: {
handler: function (val, oldVal) { /* ... */ },
deep: true
},
// 该回调将会在侦听开始之后被立即调用
d: {
handler: 'someMethod',
immediate: true
},
// 你可以传入回调数组,它们会被逐一调用
e: [
'handle1',
function handle2 (val, oldVal) { /* ... */ },
{
handler: function handle3 (val, oldVal) { /* ... */ },
/* ... */
}
],
// watch vm.e.f's value: {g: 5}
'e.f': function (val, oldVal) { /* ... */ }
}
注意:本文章使用的 vue 版本为 2.6
vue 在初始化时执行 initWatch 方法,注册 watch:
function initState (vm: Component) {
// 省略很多代码
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)
}
}
}
在 initWatch 函数中,遍历 watch 对象,当对象属性为数组时,则遍历数组并执行 createWatcher 方法;如果对象属性不为数组,则直接执行 createWatcher 方法:
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)
}
判断传入的 handler 是否为对象或字符串,实际上是兼容两种 watch 的写法:
{
watch: {
name: {
handler() {}
},
age: 'handler'
}
}
最终 handler 被设置为 watch 的回调函数;createWatcher 最后返回了 vm.$watch
函数,接着追踪 $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()
}
}
}
可以看到 $watch
是 vue 原型链上的一个方法:设置 option.user 为 true,并创建一个观察者实例 Watcher,接着,若是 options.immediate 为 true,则执行回调函数,这也就是 watch 回调函数立即执行的原理。
观察者 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.cb = cb
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
}
}
this.value = this.lazy
? undefined
: this.get()
}
分别将 options 的 deep 和 user 属性赋值给 Watcher 的 deep 和 user 属性,将 cb 回调函数赋值给 Watcher 的 cb 属性;再判断 expOrFn 是否为函数,此时 expOrFn 为 watch 的属性名,因此为字符串类型,因此将会执行 this.getter = parsePath(expOrFn)
,下面是 parsePath 的源码:
const unicodeLetters = 'a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD'
const bailRE = new RegExp(`[^${unicodeLetters}.$_\\d]`)
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
}
}
parsePath 的逻辑非常简单,使用字符串的 split 方法,以 . 作为分割点,对 path 进行切割,并生成属性数组,最终返回函数,当函数执行时,便会对传入的对象进行深度层级访问,并返回最终的属性值;举个简单的例子,假设 path 为 “a.b”,那么函数执行时会先访问 obj.a,再访问 obj.a.b~
回到 Watcher 构造函数,此时 this.getter 的值为 parsePath 生成的函数,接着往下看:
this.value = this.lazy
? undefined
: this.get()
this.lazy 为 false,于是执行 this.get 函数,并将返回值缓存到 this.value 中:
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
}
get 函数便是关键了,首先执行 pushTarget(this)
将当前的观察者 Watcher 设置为 Dep.target,Dep.target 是一个用于存储 Watcher 的全局变量;接着执行 this.getter 方法,对监听属性进行层级访问,触发各级属性的 get 方法,将当前 Dep.target 收集到对应属性的 dep.subs 数组中,实现依赖收集;而若是 this.deep 为 true,则会执行 traverse 方法,对 value 进行深度访问,触发 value 所有属性的 get 方法,实现深度监听;
当监听属性发生变化时,便会触发相应数据的 set 方法,执行属性的 dep.notify 方法,通知 dep.subs 中收集到的所有观察者,并执行 watcher.update 方法:
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
lazy 和 sync 都为 false,因此直接执行 queueWatcher 函数:
function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
queueWatcher 函数执行时,先将观察者推入 queue 队列中,再执行 nextTick(flushSchedulerQueue)
,nextTick 会在当前事件循环结束后调用 flushSchedulerQueue,因此我们简单看看 flushSchedulerQueue 方法做了什么:
function flushSchedulerQueue () {
// ...省略
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
watcher.run()
}
// ...省略
}
函数调用了 watcher.run 方法:
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)
}
}
}
}
通过执行 this.get() 获取监听属性的值,再判断值和缓存的值是否相等,不同的话执行 this.cb.call(this.vm, value, oldValue),也就是 watch 设置的回调函数!
以上就是 watch 实现监听的原理啦~
vue 中 watch 对数据进行监听的原理为:
多谢观看~请点个赞o( ̄▽ ̄)d