通过上一篇文章《Vue源码之渲染watcher》我们学习到了每个组件实例初始化时都会创建一个渲染watcher来监控页面引用到的响应式数据的变动,一旦数据发生变化,就会通知渲染watcher来重新生成虚拟DOM,做diff,update,然后patch来更新页面。
今天我们将会学习下vue框架用到的第三个watcher - 用户watcher。下面回忆下这三个watcher的简介:
既然用户watcher是根据我们编写的watch选项而创建的,所以我们下面先简单介绍下watch的用法。
我们在编写组件代码时,如果我们需要监控某个响应式属性的变化,我们会在watch选项下面实现相关的监听函数。
标准的写法应该是下面这个样子的
watch: {
counter: {
handler: function (newVal, oldVal) {
console.log(newVal, oldVal);
},
},
},
如果不需要提供immediate,deep等选项的话,可以简化写成下面这样
watch: {
counter: function (newVal, oldVal) {
console.log(newVal, oldVal);
},
},
一个完整的vue实例初始化例子如下
<!DOCTYPE html>
<html lang="en">
<head>
<script src="vue.js"></script>
</head>
<body>
<div id="app">
<div>{{counter}}</div>
<button @click="increase">Increase</button>
</div>
<script>
const vm = new Vue({
el: "#app",
data: {
counter: 1,
},
methods: {
increase() {
this.counter += 1;
},
},
watch: {
counter: {
handler: function (newVal, oldVal) {
console.log(newVal, oldVal);
},
},
},
});
</script>
</body>
</html>
下面开始分析下我们写的watch选项是怎么生成用户watcher的。
我们编写vue实例或者组件实例的时候,比如上面的创建vue实例中的例子,在写好watch选项之后,vue会将这些选项放到options参数并传递给他_init方法来对实例进行初始化。然后会经历一系列的初始化函数调用流程。
// core/instance/index.js文件
function Vue(options) {
this._init(options);
}
// core/instances/init.js文件
Vue.prototype._init = function (options?: Object) {
const vm: Component = this;
...
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm)
...
initState(vm);
}
// core/instance/sate.js文件
export function initState(vm: Component) {
vm._watchers = []
const opts = vm.$options
....
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
在_init方法中会对我们传入的选项配置进行处理,然后放入到实例的$options选项中。到了initState时,就会以我们编写的watch配置选项作为参数调用initWatch方法对用户watcher进行初始化。
function initWatch(vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
...
createWatcher(vm, key, handler)
}
}
这里会直接调用createWatcher方法
function createWatcher(
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
...
return vm.$watch(expOrFn, handler, options)
}
这里留意下handler处理部分,如果我们的watch的回调函数写法是标准带handler的那种写法的话,需要将配置项里面的handler拿出来作为本函数里面的handler函数。
跟着就是调用vue的原型函数$watch方法
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
...
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
...
}
这里注意options.user = true,这就是我们这个要创建的watcher叫做用户watcher的原因。
在调用Watcher构造函数的时候,我们这次提供了四个参数,值得留意的是,相比上两篇文章分析的渲染watcher和计算属性watcher,我们这次提供了第三个参数,即回调函数,该函数将会在依赖的响应式属性变化时被watcher执行。而这个cb,就是我们上面的handler,也就是我们自己写的那个watch的handler函数。
下面看下用户watche的构造过程
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm;
...
// options
if (options) {
...
this.user = !!options.user;
}
this.cb = cb;
...
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
...
}
this.value = this.lazy ? undefined : this.get();
}
Watcher的构造函数其实我们看了很多遍了,只是为了方便分析,针对不同使用情况下的watcher,我们就会把和它不相关的代码给省略掉。这里也一样,我们这里只保留和今天学习用户Watcher相关的核心代码。
下面我们将再次看下get成员方法的代码
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;
}
这个方法我们此前的文章也分析过很多次了,目的就是调用getter,即读取下我们监控的响应式属性,在我们的示例中,就是读取一下vm中的count,从而触发其getter,进而将我们的用户watcher加入到该响应式属性的依赖deb.subs中,即完成依赖收集过程。
如此一来,在下次有人修改了该响应式数据之后,将会遍历该响应式数据的所有订阅者,即所有依赖的watcher,包含这里的用户watcher,然后通知这些watcher去做事情,或者是像计算属性watcher那样去更新数据,或者像渲染watcher那样去重新计算虚拟DOM然后更新页面,或者是我们这里的用户watcher,则会重新执行下我们自己编写的handler回调。
以上,就是用户watcher的源码的简单分析。而这一系列的三个不同用途的watcher的源码分析也就告一段落了。多谢大家的观看和支持吧。