Vue源码学习之Watcher

Vue源码学习之Watcher

上面一章我们学习到Vue的构造函数的初始化和Vue的响应式的工作原理,但是Vue中数据收集依赖的另外一个重要部分:Watcher。
为了解析Watcher我们先来了解一下computed函数:

1
2
3
4
5
6
7
8
data: {
name: "Ming"
},
computed: {
message() {
return `RNG ${this.name}`;
}
},

这里有两个data.namedata.message自定义数据,通过message的数据依赖于name;我们在第一章的Demo中可以看到,当name的值改变时,对应的message也会发生改变;name值改变我们在上一章说道是通过Observer来重新定义属性值对其实现监听。

data.message是怎样与Observer建立关联来实现其值的改变。Watcher是把两者关联在一起的关键。我们首先看看initComputed的函数,再看看Watcher里面是如何工作的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function initComputed (vm, computed) {
// $flow-disable-line
var watchers = vm._computedWatchers = Object.create(null);
// computed properties are just getters during SSR
var isSSR = isServerRendering();// 判断是否是服务器端渲染

for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if (getter == null) {
warn(
("Getter is missing for computed property \"" + key + "\"."),
vm
);
}

if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}

// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else {
if (key in vm.$data) {
warn(("The computed property \"" + key + "\" is already defined in data."), vm);
} else if (vm.$options.props && key in vm.$options.props) {
warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
}
}
}
}

排除一些警告函数和if判断后,从初始化computed函数中可以看出,先会对computed函数中的值进行创建Watcher实例,然后判断属性是否存在vm实例中,如果没有,通过函数defineComputed重新定义属性的getter/setter,接下来看看defineComputed里面是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function defineComputed (target,key,userDef) {
var shouldCache = !isServerRendering();
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef);
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
if (sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
("Computed property \"" + key + "\" was assigned to but it has no setter."),
this
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}

在这个函数里面重新定义属性key,主要是做重新定义属性的getter/setter函数,对象sharedPropertyDefinition就是如此。

1
2
3
4
5
6
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};

在判断条件中看看怎样对get函数进行重置,重置中createComputedGetter函数主要是做了什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}

该函数用于创建Getter函数,返回一个函数,当触发message值的获取时,同时调用到属性的getter函数,从这里看到获取当前Watcher实例,同时将Dep.target置为当前的属性的watcher。让data中的getter中能获取当前属性的值并且赋值到当前属性的watcher中的value值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Depend on all deps collected by this watcher.
*/
Watcher.prototype.depend = function depend () {
var i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
};

Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};

这里还要扯一句,那么watcher实例创建中是如何工作的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 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.
*/
var Watcher = function Watcher (vm,expOrFn,cb,options,isRenderWatcher) {
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$1; // 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 = expOrFn.toString();
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
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();
};

前面一些代码其实就是构造函数的一些属性的初始化,主要的属性:getter、deps、dirty等,我看看最后一行代码:

1
2
3
this.value = this.lazy
? undefined
: this.get();

在Vue中,Watcher 不止会监听 Observer ,而且他会直接把值计算出来放在 this.value 上。虽然这里因为 lazy 没有直接计算,但是取值的时候肯定要计算的,所以我们直接看看 getter 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Evaluate the getter, and re-collect dependencies.
*/
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var 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
};

首先我们看到pushTarget函数,这个函数是将当前的watcher置为全局变量,让data中的getter函数可以访问的到。

1
value = this.getter.call(vm, vm);

前面在创建watcher实例会创建属性getter,会将computed中message函数赋值给getter,触发去调用该属性的getter方法computedGetter,去获取当前属性的计算值。
但是触发watcher实例的属性getter,就是相当于计算属性message:

1
2
3
message() {
return `RNG ${this.name}`;
}

就会触发this.name的getter函数,使得当前的watcher实例添加到dep中,完成依赖收集。在此之前,会执行pushTarget函数,将当前的watcher置为全局变量,让其在this.name的gtter函数中访问到。同时计算属性对应的Watcher是懒加载的,他会在第一次解析到模板时,计算属性才会建立依赖关系,并计算出值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!--在getter函数中代码-->
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
}
if (Array.isArray(value)) {
dependArray(value);
}
}
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};

Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};

到现在为此,我们弄懂了当this.message读取值,就会进行依赖收集。当this.name的值发生改变时,监听对象watcher中的value值是怎样改变的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--属性的setter函数代码-->
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}

最后一行代码 dep.notify()会通知所有的watcher去更新数据,notify代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (!config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};

如果是异步模式,那么会执行run方法去更新value值。否则会进入到queueWatcher(this);,等到执行nextTick之后才会执行run。这也是我们为什么更新了value之后立刻读取value,其实内容并没有被更新的原因。因为把所有的更新都集中到nextTick 进行,所以Vue会有比较好的性能。queueWatcher方法会用一个队列记录所有的操作,然后在nextTick的时候统一调用一次。

总结

这一节和上一章基本上讲完了Vue的数据响应的原理,讲解了依赖收集的工作原理。接下来的一节主要讲解render函数和watcher和render之间的联系。


本文由 Abert 创作,采用 知识共享署名 4.0 国际许可协议。

本站文章除注明转载/出处外,均为本站原创或翻译,转载请务必署名。