Appearance
Watcher
javascript
function parsePath(path: string): any {
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
class Watcher implements DepTarget {
vm?: Component | null
expression: string
cb: Function
getter: Function
value: any
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null
) {
// 处理 options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.deps = [] // watcher 收集的 deps
this.newDeps = [] // 每次 updateComponent 都会有一个新的 deps
this.depIds = new Set() // 保存收集过的 dep 的 id,用于 dep 去重。
this.newDepIds = new Set() // 保存新的收集过的 dep 的 id,用于 dep 去重。
if (isFunction(expOrFn)) {
// 对于 render watcher 来说 expOrFn 就是 updateComponnet,是函数。
this.getter = expOrFn
} else {
// 对于 user watcher 来说,expOrFn 是监视的属性,是字符串,这样就区分开
// render watcher 和 user watcher 了。
// user watcher 的 getter 方法要实现监视属性值变化时,执行回调。
// 那么只需要让属性的 dep 收集 user watcher,即可。
// 我们直接去 vm 上访问这些属性,触发 Object.defineProperty 中的 get
// 从而让 dep 和 user watcher 互相收集。由于属性可能是内层属性,
// path.split('.') 不断取值向下找即可,最后返回这个值,这个值就是 oldValue。
// 由于 user watcher 接收 newValue、oldValue 两个参数。当执行 update 时,
// 会执行 queueWatcher 从而执行每个 user watcher 的 run 方法。run 方法中
// 我们先用变量保存 this.value,再将调用 this.get() 的返回值挂载到 this.value
// 上,就得到了 newValue 和 oldValue,最后通过 this.user 判断更新的是不是
// user watcher,如果是就 cb.call(this.vm, newValue, oldValue) 即可。
this.getter = parsePath(expOrFn)
}
this.value = this.get()
}
get() {
// 记录当前 watcher
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e: any) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// 清除记录
popTarget()
// 更新收集的 dep
this.cleanupDeps()
}
return value
}
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run() {
if (this.active) {
const value = this.get()
const oldValue = this.value
this.value = value
if (this.user) {
this.cb.call(this.vm, value, oldValue)
}
}
}
}