主题
Watcher
watch 分类
提示
在 Vue 中, 有三种类型的watcher
实例, 分别为:
watch watcher
: 被监听的属性值变化后,执行对应的回调函数computed watcher
: 用于控制计算属性是否需要重新计算render watcher
: 用于 vue 组件渲染
Watcher
定义在src/core/instance/observer/watcher.ts
中
Watcher
源码如下:
ts
let uid = 0;
export default class Watcher implements DepTarget {
vm?: Component | null;
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;
onStop?: Function;
noRecurse?: boolean;
getter: Function;
value: any;
post: boolean;
// dev only
onTrack?: ((event: DebuggerEvent) => void) | undefined;
onTrigger?: ((event: DebuggerEvent) => void) | undefined;
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
recordEffectScope(
this,
// if the active effect scope is manually created (not a component scope), prioritize it
activeEffectScope && !activeEffectScope._vm
? activeEffectScope
: vm
? vm._scope
: undefined
);
if ((this.vm = vm) && isRenderWatcher) {
vm._watcher = this;
}
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
if (__DEV__) {
this.onTrack = options.onTrack;
this.onTrigger = options.onTrigger;
}
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid; // uid for batching
this.active = true;
this.post = false;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();
this.expression = __DEV__ ? expOrFn.toString() : "";
// parse expression for getter
if (isFunction(expOrFn)) {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
__DEV__ &&
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); // 给 Dep.target 赋值, 也是将watcher实例入栈
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 {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value); // 递归遍历对象以调用所有转换后的 getter,以便将对象内的每个嵌套属性收集为“深度”依赖项。
}
popTarget(); // 收集完依赖以后将watcher出栈
this.cleanupDeps(); // 清除依赖集合
}
return value;
}
/**
* 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);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps() {
let i = this.deps.length;
while (i--) {
const dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this);
}
}
let tmp: any = 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.
*/
update() {
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 "${this.expression}"`;
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.
*/
depend() {
let i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown() {
if (this.vm && !this.vm._isBeingDestroyed) {
remove(this.vm._scope.effects, this);
}
if (this.active) {
let i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
this.active = false;
if (this.onStop) {
this.onStop();
}
}
}
}
vue 中三种 Watcher 都是服用发这一个 Watcher 类,可能计算属性或者监听器相对来说更好理解,所以先从这两者来分析 Watcher 类
一. 监听器 watcher
监听器的初始化过程见下一篇《监听器 watch 原理探索》
1. constructor
对于监听器 watch 而言,在业务代码中定义的时候 里面是一个对象{}
,因此 key 都为 string,所以const watcher = new Watcher(vm, expOrFn, cb, options);
里面 expOrFn 一直都为 string, cb 为函数,当某个 watch 为对象的时候 options 为这个对象,内部含有 immediate handler deep 属性,若为函数则 options 为 undefined
因此在Watcher
的 constructor 中对 expOrFn 做兼容处理的逻辑会走到else
内部
ts
// parse expression for getter
if (isFunction(expOrFn)) {
this.getter = expOrFn;
} else {
// watch Watcher走的是这里
this.getter = parsePath(expOrFn); // 解析并根据正则校验,当前watch的key是否符合命名规范
if (!this.getter) {
this.getter = noop;
__DEV__ &&
warn(
`Failed watching path: "${expOrFn}" ` +
"Watcher only accepts simple dot-delimited paths. " +
"For full control, use a function instead.",
vm
);
}
}
this.getter = parsePath(expOrFn)
是对watch
的 key 命名做校验的,校验通过则为 key 创建并返回一个 getter 函数, 其实就是当前 key 在 vm 中的定义,为依赖收集做准备
parsePath 方法位于src/core/util/lang.ts
,源码如下
ts
const unicodeRegExp =
/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/;
/**
* Parse simple path.
*/
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`);
export function parsePath(path: string): any {
if (bailRE.test(path)) {
return;
}
const segments = path.split(".");
// 为当前的watch创建一个getter函数,用于后续依赖收集
return function (obj) {
// 此函数可以看做是getter函数, obj就是后面会传入的vm
for (let i = 0; i < segments.length; i++) {
if (!obj) return;
obj = obj[segments[i]]; // 这里obj === vm
}
return obj;
};
}
::: unicodeRegExp.source 解释 RegExp.prototype.source
source
属性返回一个值为当前正则表达式对象的模式文本的字符串,该字符串不会包含正则字面量两边的斜杠以及任何的标志字符。 :::
源码中是用.
对属性进行拆分的, 这也是为啥在 watch 中可以监听xxx.xx.xxxx
这样的属性的原因,vue 最终或将其变成 vm.xxx.xx.xxxx
这样的形式
上述源码的结果若是能正确运用,会为当前 watch 中的某个 path 即(expOrFn)创建一个 getter 函数并返回, 这个函数会在后续过程中被 vm 调用且传入 vm 作为参数,目的是为了收集依赖.
这个 getter 函数内部的本质就是取到真正要监听的属性在 vm 对象中目标层级上的函数或属性定义, 调用它就等价于在调用 vm 身上某个属性的 getter 进行取值的过程
例如:
js
export default {
data() {
return {
pageUrl: ""
};
},
watch: {
pageUrl(val, oldVal) {
/* .......... */
},
'$route.path':{
handler()
}
}
};
上述代码中的:
- key 为
pageUrl
的监听器创造了一个 getter, 这个 getter 的内部其实就是执行 vm.pageUrl 的过程 - key 为
$route.path
的监听器也创造的一个 getter,这个 getter 的内部是执行 vm.$route.path 的过程
回到consturctor
源码中,得到 key 的 getter 函数后,将其复制给 Watcher 的实例,然后做了一步取值操作,且利用取值的操作完成了 watch 依赖收集
js
this.value = this.lazy ? undefined : this.get();
由于监听器 Watcher 没有 lazy 属性, 所以会直接走到 this.get()
的调用过程,get 函数就是依赖收集的过程,下面继续分析
2. get 依赖收集
constructor
的最后,调用this.get()
方法,get 方法的源码如下:
ts
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
pushTarget(this);
let value;
const vm = this.vm;
try {
value = this.getter.call(vm, vm); // this.getter内部就是 vm.xxx, 因为vm.xxx会触发xxx的getter
} catch (e: any) {
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;
}
首先,当前的 watcher 实例 push 进入targetStack
,targetStack
是在 Dep 里面定义的,接着从实例上拿到 vm,并用 vm 调用 getter 函数取值且,将 vm 作为参数传进去
ts
pushTarget(this);
const vm = this.vm;
value = this.getter.call(vm, vm);
上一步已经分析出来了, 这里的this.getter
函数就是上一步constructor
里面parsePath
方法得到并挂到this
上面的,并且知道在调用这个this.getter
函数其实就是在执行vm.xxx
, 那么在initWatch
之前, vue 已经完成了 initProps
initMathods
initData
, 并且已经完成了将props
和data
内的属性用Object.defineProperty
进行拦截, 所以此处在调用this.getter
函数进行属性取值的时候, 其实就是为了利用对 watch 的 key 属性进行 vm 取值的这个过程,触发这个 key 属性的 getter
这里贴一段 Object.defineProperty 中 getter 的源码,便于分析
ts
// Object.defineProperty 的get函数定义, 源码位置: src/core/observer/index.ts
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
// 由于上面 已经 pushTarget(this); 因此这里为true
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key
});
} else {
dep.depend();
}
if (childOb) {
childOb.dep.depend();
if (isArray(value)) {
dependArray(value);
}
}
}
return isRef(value) && !shallow ? value.value : value;
}
看到除了正常取值外就是依赖收集的逻辑
取值就不说了, 取完值后用 value 变量存起来后, 判断了 Dep.target 是否有值, target 是 Dep 类的静态属性, 上一步的pushTarget
就是为其赋值的,因此这时候汇金到if
里面
ts
// Dep
Dep.target = null;
const targetStack: Array<DepTarget | null | undefined> = [];
export function pushTarget(target?: DepTarget | null) {
targetStack.push(target);
Dep.target = target;
}
if
里面首先执行的是 dep.depend()
,然后检查有 childOb,childOb 表示 children 有被观察过,则也调用子级的 depend 方法,若 vm.xxx 为数组,则递执行前面逻辑
Dep.property.depend 方法源码如下:
ts
// Dep实例方法
depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
Dep.target.addDep(this)
if (__DEV__ && info && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
...info
})
}
}
}
同样, 这里面的 Dep.target 是有值的, 且值就是当前这个侦听器的一个watcher
实例
depend 内部就仅做了一件事, 调用了Watcher
原型上的addDep
,并将dep
实例作为参数传入
js
// Dep实例方法depend内部
// Dep.target为Watcher实例
if (Dep.target) {
Dep.target.addDep(this); // this为dep, 因为这代码是在Dep的实例方法depend中执行的
}
Dep.target
为Watcher
实例this
为 Dep 实例dep
, 因为这代码是在Dep
的实例方法depend
中执行的
接下来在来看一下Dep.target.addDep(this)
也就是 watcher.addDep(dep)
的源码:
ts
// Watcher实例方法: /src/core/observer/watcher.ts
/**
* Add a dependency to this directive.
*/
addDep(dep: Dep) {
const id = dep.id // 每一个dep实例都有的一个唯一的标识
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
addDep
的源码中会先检测当前watcher
的newDepIds
中是否包含传入的 dep 的 id,若存在则什么也不做,不存在才会接着往下执行,是如何判断的呢?
首先,每一个 Dep 的实例上都会有一个唯一的 id 属性,在new Dep
的时候,会进行 id 的累加
ts
let uid = 0;
class Dep {
constructor() {
this.id = uid++;
}
}
其次,每一个监听器 watch 中的每一项属性 都会对应一个或者多个 Watcher
实例watcher
, 且每一个watcher
自身上都会有一个newDepIds
和一个depIds
,他们都是new Set()
,还有一个deps
与newDeps
,他们是数组[]
,每一项,为何要这样设计呢?
前面说过, watch
的值可以是一个数组,那么一个key
可以对应多个处理函数,每一个处理函数最终都会对应一个Watcher
实例,当被监听的 key 变化后,会执行这一些处理函数,
同样,每一个 watcher 都有一个唯一的 id,原理同dep.id
一样