|
继 Observer 和 Dep 类之后,我们迎来了这三个类中最复杂的类——Watcher。Watcher 这个词在 Vue 中有很多叫法:观察者、依赖者及订阅者等,我觉得它们的叫法都挺有道理。Watcher 就像一个哨兵,时刻观察着所需的变量,一有变动就通知其他部件,这里我们也称它为观察者。先来一张 UML 图熟悉下
UML

vue Watcher类uml
使用场景
我们先来看看都有谁使用 Watcher 创建了对象。我们在 Vue 的源码里全局搜索 new Watcher,有三处地方创建了 Watcher 对象,分别是:
1、文件 /src/core/instance/lifecycle.js 的 mountComponent 函数中
...
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
...
2、文件 /src/core/instance/state.js 的 initComputed 函数中
...
var computedWatcherOptions = { lazy: true };
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
...
3、文件 /src/core/instance/state.js 的 Vue.prototype.$watch 原型方法中
Vue.prototype.$watch = function (expOrFn: string | Function,
cb: any,options?: Object): Function {
...
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
...
}
它们分别用于渲染函数、计算属性(computed)和侦听属性(watch),这三者都是需要在特定变量更新时作出响应。这在观察者模式中,变量是被观察者,Watcher 就代表观察者。也可以说是发布-订阅模式,变量及关联的 Dep 对象是发布者,Watcher 是订阅者。
Watcher
下面我们来看看 Watcher 的源码,它在文件 /src/core/observer/watcher.js 中定义
...
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
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, // Vue类/组件 实例
expOrFn: string | Function, // 字符表达式或函数
cb: Function, // 回调函数,收到更新通知时执行
options?: ?Object, // 其他选项
isRenderWatcher?: boolean // 是否为渲染 watcher
) {
this.vm = vm
if (isRenderWatcher) { // 如果是渲染 watcher,对象赋值给 vm._watcher
vm._watcher = this
}
vm._watchers.push(this) // 对象放入 vm 的 _watchers 中
// 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 // 实例ID
this.active = true
// 初始化 dirty = lazy,主要用于计算属性
this.dirty = this.lazy // for lazy watchers
this.deps = [] // 当前观察的 dep
this.newDeps = [] // 新收集的需要观察的 dep
this.depIds = new Set() // deps 的ID集合
this.newDepIds = new Set() // newDeps 的ID集合
this.expression = process.env.NODE_ENV !== &#39;production&#39;
? expOrFn.toString()
: &#39;&#39;
// parse expression for getter
// expOrFn 转成 getter 函数
if (typeof expOrFn === &#39;function&#39;) {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== &#39;production&#39; && warn(
`Failed watching path: &#34;${expOrFn}&#34; ` +
&#39;Watcher only accepts simple dot-delimited paths. &#39; +
&#39;For full control, use a function instead.&#39;,
vm
)
}
}
// 如果不是 lazy 的 watcher 则立即执行 get 成员方法
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
// 调用 getter,并且收集依赖
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 &#34;${this.expression}&#34;`)
} else {
throw e
}
} finally {
// &#34;touch&#34; every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
/**
* Add a dependency to this directive.
*/
// 添加 dep
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
/**
* Clean up for dependency collection.
*/
// 清除旧的 dep,新的 dep 赋给 deps
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
// dep派发更新通知时执行
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* 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) {
const info = `callback for watcher &#34;${this.expression}&#34;`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
// 求值方法,主要在 计算属性 中用
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
// 调用本 watcher 拥有的所有 dep 的 depend 成员方法
depend () {
let i = this.deps.length
while (i--) {
this.deps.depend()
}
}
/**
* Remove self from all dependencies&#39; subscriber list.
*/
// watcher 被销毁,清理相关配置
teardown () {
if (this.active) {
// remove self from vm&#39;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.removeSub(this)
}
this.active = false
}
}
}
构造函数
Watcher 类有点长,我们先来看构造函数,首先它根据传入构造函数的第五个参数来确定该 Watcher 对象是否为渲染 Watcher,每一个组件都有一个渲染 Wathcer,如果是则把它赋值给 vm._watcher 并放入组件的数组属性 vm._watchers 中。
然后初始化选项,user 是否通过侦听属性创建的 Watcher;lazy 是否为懒求值的 Watcher,通常用于计算属性的延迟求值;dirty 是否脏状态,也是用于延迟求值;cb 通常用于侦听属性的回调函数;sync 接收到更新通知时是否同步执行调度方法;depIds 和 newDepIds 分别为现有 Dep ID 和 新收集的 Dep ID;deps 和 newDeps 分别为现有 Dep 和新收集的 Dep 等。
跟着把传参 expOrFn 转换为 getter 函数,如果 expOrFn 是字符串则通过调用 parsePath 函数转换成函数,它主要是按点切分为属性键用来访问对象的属性值。正如上面提到的,三处不同地方实例化对象时传入的 expOrFn 不同导致各自的 getter 各异,但是它们本质都是引用响应式变量,从而触发依赖收集。
最后判断 this.lazy 的值,假则执行 get 成员方法并返回值赋给 this.value,否则直接赋值 undefined。
get 方法
成员方法 get 是这个类中非常重要的方法,它主要是调用 getter 函数,并在其执行过程中触发依赖收集。我们先用伪代码简单描述一下
// 入栈,备份当前 Dep.target,然后设置为本 Watcher 对象
pushTarget(this)
// 调用 getter 求值,触发响应式变量的 getter 收集依赖
value = this.getter.call(vm, vm)
// 如果需要,则对 value 的属性递归求值和收集依赖
traverse(value)
// 出栈,恢复 Dep.target 为之前备份的 Watcher 对象
popTarget()
// 新旧依赖过滤,移除不需要的依赖
this.cleanupDeps()
pushTarget 与 popTarget 函数我们在上一节学习 Dep 类的时候有说到,并且还有生动形象地配图。这里它 pushTarget(this) 把自身对象设置到了全局唯一变量 Dep.target,然后调用 this.getter,这个函数简单的理解成引用变量即可,在引用变量的时候会执行在 Object.defineProperty 中配置的变量的 reactiveGetter 函数,并在里面收集依赖。
...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 获取属性的值
const value = getter ? getter.call(obj) : val
// --------------------------------
if (Dep.target) { //
dep.depend() //
if (childOb) { //
childOb.dep.depend() // 依赖收集
if (Array.isArray(value)) { //
dependArray(value) //
} //
} //
} //
// --------------------------------
return value
},
set: function reactiveSetter (newVal) {...}
...
}
然后如果需要深度收集的话,会递归调用属性值求值并收集依赖,完成这一系列之后执行 popTarget 恢复 Dep.target。再之后就是通过调用 cleanupDeps 过滤清理新旧依赖关系,这个过程在方法 cleanupDeps 中细说。
addDep 方法
这个方法是用于依赖收集中的,如上代码段,响应式变量的 reactiveGetter 方法中,依赖收集是判断 Dep.target 为真,即是 Dep.target 为 Watcher 对象,则调用变量关联的 Dep 对象的 depend 方法。
// Dep 类 成员方法 depend
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
它相当于又是调用 Watcher 对象的 addDep 方法,并把 Dep 对象传入。
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
// 新的Deps中,且旧的Deps没有,则加入Dep的subs中
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
addDep 检查如果传入的新 Dep 的 id 不在 newDepIds 中则加入并把 Dep 对象加入 newDeps 中。再判断如果 id 不在 depIds 中则调用 Dep 的 addSub 方法,参数为本 Watcher 对象,这个 addSub 只是把 Watcher 对象放入 Dep 的 subs 数组中,这样 Dep 就拥有了订阅了它的所有 Watcher 对象。

vue Dep与Watcher关联
cleanupDeps 方法
在 get 方法中有看到,这个方法是在 popTarget 恢复栈之后被调用,这个时候对于 getter 中新一轮收集的依赖已经通过 addDep 全部暂存于 newDeps 中了,它们的 ID 也存于 newDepIds 中。
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps
// Dep中删除旧的订阅
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
cleanupDeps 方法首先遍历 this.deps 里面旧的 Dep,如果其 ID 不在新的 newDepIds 中,则调用 Dep.removeSub 从 Dep.subs 订阅数组中删除该 Watcher,这样该 Watcher 就不会再接收到更新通知。后面的8行代码,就是对调 newDeps 和 deps,depIds 和 newDepIds,然后清空 newDeps 和 newDepIds,那么,有没有简单的写法呢?为什么又要写这么复杂呢?
this.depIds = this.newDepIds
this.newDepIds = new Set()
this.deps = this.newDeps
this.newDeps = []
这4行代码与上面最后8行代码是等效的,比之却简单了很多,但是 Vue 的写法很大程度降低了频繁地创建对象,不管对调多少次,始终都是用的在构造函数中创建的 Set 对象和数组,这是 Vue 代码的一个很好的细节。
update 方法
这个方法很好理解,它是供 Dep 在派发更新通知时主动调用的,它根据不同选项做不同处理,如果 lazy 和 sync 都为假,则通过调用 queueWatcher 把自身放入调度队列,在下一个周期执行调度任务 run 方法。简单来说就是计算属性 watcher 是把 dirty 设为 true,不执行回调;渲染 watcher 和 侦听属性 watcher 则是把 watcher 对象放入队列进行调度。
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
// 如果 lazy 为真(延迟求值)
if (this.lazy) {
// 设置 dirty 为真,后续在 evaluate 中计算求值
this.dirty = true
} else if (this.sync) {
// 如果 sync 为真,同步执行调度任务 run 方法
this.run()
} else {
// 把本对象加入调度队列
queueWatcher(this)
}
}
run 方法
可以称为调度任务,在 update 执行 queueWatcher 放入队列后,在适当的时机会执行该方法,这个细节我们后续会在其他章节说。这个方法会执行方法 get,对如渲染 Watcher 则重新渲染页面并收集依赖,然后比对返回值并判断其他条件觉得是否执行回调函数 this.cb,对于满足条件的侦听属性的回调函数也是在这个阶段被执行。
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
// 调用 this.get 求新值
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 &#34;${this.expression}&#34;`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
// 执行回调函数
this.cb.call(this.vm, value, oldValue)
}
}
}
}
evaluate 方法
这个方法用在计算属性的响应式 computedGetter 中,在文件 /src/core/instance/state.js 中见代码
function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
对于计算属性的初始化内容前面我们有学习,计算属性的 Watcher (computedWatcher) 在实例化时 lazy 选项是为真的,那么初始化时 dirty 也为真,这就导致了在 update 和 run 方法中都不会马上调用 get 求值,而是把 dirty 设为 true,并且在引用计算属性的时候,调用计算属性的 computedGetter,这个时候 evaluate 就被调用求值了,并把 dirty 设为 false。
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
depend 方法
是的,没有看错,Watcher 类里面也有一个 depend 方法,这个方法与 evaluate 同样是在计算属性的 computedGetter 中被调用的(computedGetter 代码见 evaluate 方法),当 Dep.target 不为空时则调用它。
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps.depend()
}
}
前面文章“计算属性与侦听属性初始化浅析”中我们研究过计算属性的响应式定义,我们看看它的图简单回顾下:

vue 计算属性初始化
这个 depend 中会遍历所有依赖的 Dep 并执行其 depend 方法,这有点绕。没关系,其实它的本质就是把计算属性 Watcher(computedWatcher)依赖的所有 Dep 同样也让 Dep.target 依赖之。因为在定义响应式计算属性的时候没有对应关联的 Dep 对象(不像 data 属性),所以 Dep.target 无法收集对计算属性的依赖。computedWatcher.depend() 之后,Dep.target 对于计算属性的 Dep 的依赖转移到对 computedWatcher 依赖的 Dep 上。

vue 计算属性依赖收集
Dep.target 在依赖计算属性失败后,转而依赖计算属性依赖的 Dep,这样与直接依赖计算属性是一样的,而且这样还有个好处就是当计算属性依赖的变量有改变时,Dep.target 就能收到更新通知并做必要的响应。
teardown 方法
这个方法是在组件销毁或者unwatch($watch方法返回)时调用,它的作用比较简单,就是把自身从 vm._watchers 中移除,并遍历全部依赖的 Dep,调用其 removeSub 从 Dep 的 subs 订阅列表中移除自身。
总结:
渲染函数,计算属性和侦听属性等都有需要监视变量变动的需求,Watcher 正是在这种情况下应运而生,Watcher 作为观察者,配合 Dep 一起组成响应式的核心部件。这个类代码量比较多,我们也花了很多笔墨来研究学习,我觉得这是值得的。接下来我们会把前面学的这些类串起来,学习整个响应式的核心流程与逻辑。 |
|