前篇
上一章,我们对Observer进行了讲解,从observe入口文件开始,到Observer类的声明和调用,最后讲解了Dep关于依赖的部分,链接在此Vue2源码解读(四)-Observe,本章咱们将继续讲解Observer,不过这篇的侧重点是Watcher,最后把Observer进行串起来,方便读者们进行理解。
正文
正文从这开始,Watcher文件比较长,声明也比较长,咱们先看看声明,里面的函数的具体实现;然后咱们来个例子看下监听。
Watcher声明
来看下Watcher声明的部分的源码:
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(){....}
get(){....}
addDep(){....}
cleanupDeps(){....}
update(){....}
run(){....}
evaluate(){....}
depend(){....}
teardown(){....}
}
上面是Watcher的声明,包括私有变量和方法的声明,咱们一一来解读下含义:
- vm:顾名思义,就是实例;
- expression:表达式,要监听的value的字符串表达;
- cb:回调函数,
- id:当前watcher实例的一个计数,从1开始;
- deep:是否深度监听,取值为options.deep,默认false;
- user:是否是用户触发的watcher,取值为options.user,只有$watch调用生成的watcher实例才会是true,默认为false;
- lazy:是否是懒处理,取值为options.lazy,只有computed的属性创造的实例才会是true,默认为false;
- sync:是否是同步执行,取值为options.sync,**只有watch的时候传进来的,只有传为true的时候才会为true,默认为false;
- dirty:有影响的,是否需要对此值进行重新获取;只有computed的属性创造的实例才会是true,默认为false;
- active:当前watcher是否还有效,默认值为true;
- deps:是一个存储依赖Dep的数组,默认值为[];
- newDeps:是一个存储依赖Dep的数组,默认值为[],区别在于,此次更新后,收集到的下一轮更新所相关的依赖;
- depIds:存储deps对应的dep的id所组成的一个Set,默认值为空Set;
- newDepIds:存储newDeps对应的dep的id所组成的一个Set,默认值为空Set;
- before:是一个函数,存储更新之前所需要调用的函数,取值为options.before,$watch调用生成的watcher实例有可能有值;再就是mount函数里面会有值,此处的值为调用beforeUpdate钩子函数,默认值为null;
- getter:获取监听的value的函数;
- value:值,监听的对象的值;
说明:上面所说的$watch包含new Vue()时传递的watch对象,因为initWatch方法内部还是调用的上面加粗的watch方法
看完了类的声明,咱们看下构造函数:
构造函数constructor
构造函数的作用就是对实例进行初始化,进行一些默认值的处理;
源码
constructor(vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
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
this.active = true
this.dirty = this.lazy
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
}
}
this.value = this.lazy ? undefined : this.get()
}
上面代码就是Watcher构造函数的代码。
参数解读:
- vm:实例本身;
- expOrFn:表达式或者函数,获取value使用;
- cb:回调函数;
- options:可选参数,对象类型,配置信息,可配置deep、user、lazy、sync、before等五个参数;
- isRenderWatcher:是否是渲染watcher,只有mount函数里面过来的才是true;
源码解读:
- 赋值当前实例给当前vm属性;
- 判断是否是渲染watcher,如果是,则把当前watcher赋值给vm的_watcher(声明在initLifecycle);
- 把当前watcher,存储到vm的_watchers(声明在initState);
- 对传进来的配置options进行处理,赋值给watcher实例;
- 默认值的一些设定;
- 处理expression,生产环境为空串;
- 根据expOrFn处理getter,函数的话直接赋值;否则调用parsePath,获取调用函数;
- 获取value,如果是懒处理,则暂时为undefined,否则调用get;
watcher-get
上面最后一步讲到不为懒处理则会调用get,下面咱们就来看看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 {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
上面是get函数的源码,正式的逻辑就是从此处开始,慢慢,细心,认真,一起,来读:
- pushTarget,很熟悉的函数,就是在上一篇Observer中讲到的Dep的static属性对外暴露的变更方法;此处会改变Dep.target为当前Watcher实例;
- 调用getter方法,获取value;
- 如果深度监听,调用traverse;
- popTarget,成对出现的这两个,把Dep.target返回到之前的状态;
- 调用cleanupDeps;
- 返回当前value;
调用pushTarget和popTarget,基本就是依赖收集的过程。如上代码调用getter,就会调用到Observer里面声明的defineReactive函数里面对get重新定义的部分,这时候Dep.target是有值的,所以会进行对当前watcher的收集。
watcher-cleanupDeps
上面get函数调用到了cleanupDeps函数,现在咱们就来看下cleanupDeps的实现:
cleanupDeps() {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
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函数对收集的依赖进行清理。
- 对当前deps进行遍历,如果更新后的依赖集里面不存在当前依赖,则对当前依赖调用removeSub进行删除;
- 分割线上面先对depIds进行处理,变更为newDepIds,然后把newDepIds进行清空;
- 分割线下面对deps进行处理,变更为newDeps,然后把newDeps清空;
watcher-addDep
前面在讲Dep的时候,讲到dep的depend为依赖收集的函数,此处会调用Watcher实例的addDep函数,接下来咱们看下addDep函数:
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)
}
}
}
实际上去重操作主要源自于depIds和newDepIds的类型,先是对newDepIds进行了判断,无要添加的dep,则进行添加操作(newDepIds.add 和 newDeps.push);然后对当前depIds进行判断是否有当前要添加的dep,调用addSub添加当前watcher;
watcher-update
前面在讲Dep的时候,讲到dep的循环所有的依赖(Watcher实例),然后调用实例的update方法,接下来咱们看下update函数:
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
几行代码解决update的分配问题;
- 如果是懒处理,则对当前watcher进行打标,把dirty置为true,标记为有影响的,后面需要对value重新取值;
- 如果是同步的,则直接调用run方法;
- 其他情况则调用queueWatcher方法,加入队列;
watcher-run
上面讲到第二种同步的情况下对调用run方法,下面来看下run方法的实现:
run() {
if (this.active) {
const value = this.get()
if (value !== this.value || isObject(value) || this.deep) {
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
上面即为run函数的源代码,首先对当前watcher进行了判断是否是active的,如果已经处于非active状态则不会执行;
- 调用上面讲到的get方法获取最新的value,此处会收集依赖;
- 判断是否需要进行继续往下执行,三种情况:不同值、是对象、深度监听;
- 存储旧值,给this.value赋新的值;
- 调用回调;也就是watch对象的参数;
watcher-depend&evaluate
为什么这两个方法要放在一起,因为这两个方法都是专门供给computed里面的get方法进行调用的;还记得createComputedGetter方法吗?如果忘了,可以去回顾下再看看
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
上面调用了evaluate和depend,接下来先看下evaluate;
evaluate() {
this.value = this.get()
this.dirty = false
}
上面函数只有在dirty为true时才会调用,因为上面update讲到是标记为有影响的,需要重新取值,取完值后,再把有影响的改为false;
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
因为此处将对computed的对象进行取值,调用get方法,所以会调用depend进行循环调用收集依赖;computed是比较复杂的,dep和watcher会相互调用;
实例讲解
原则:get收集依赖,set依赖调用;也就是get收集,set调用;
下面data部分的实例进行讲解,先来看下例子:
var vm = new Vue({
el: '#demo',
data() {
return {
message: 'message',
}
}
})
vm.message = 'new massage'
上面的代码,就是最简单的一个Vue的data属性message变更。
如图可以看到,当调用get的时候,会对依赖进行收集;当调用set的时候,则会调用update,并最终调用到run,run里面就是对cb(回调函数)的执行。
scheduler
文件位于/src/core/observer/scheduler.js
queueWatcher监听队列
上面watcher-update部分,讲到调用queueWatcher方法,加入队列,咱们来看下queueWatcher的实现:
function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
queueWatcher函数是一个对watcher进行队列处理的函数;变量说明一下:
- flushing:调度队列状态,队列是否在循环过程中;
- waiting:调度器的状态,是否处于等待(非空闲)状态,true说明等待中(非空闲),false说明执行完成(空闲中);
- has是一个对象,key为watcher的id,默认值为true;
好了,咱们接下来来解读下源码:
- 先判断下是否队列中已经存在当前watcher;
- 不存在则把has[watcher.id]置为true;
- 是否在循环过程中?不在循环过程中直接塞进去;在循环过程中,则插进去,插入的位置为正在执行的watcher的后面;
- 如果空闲状态,则把waitting置为true,非空闲状态;同时执行flushSchedulerQueue;
flushSchedulerQueue
上面代码讲到最后会执行flushSchedulerQueue函数,咱们接着缕着来看看flushSchedulerQueue的实现:
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
}
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
上面的代码,是经过处理后的代码,把非生产环境的代码去除掉了;
- 记录开始的时间;
- 把调度队列状态置为true;
- 对队列进行排序,id越小的watcher排的越靠前,也就是越早创建的watcher,排的越靠前;
- 对队列进行循环,before调用和run调用,同时把has[watcher.id]置为null;
- 对activatedChildren和queue进行复制;
- 调用resetSchedulerState,把队列和队列状态以及调度器状态置为初始状态;
- 调用activated和updated钩子函数;
- 浏览器针对Vue开发工具进行更新操作;
结言
两篇文章讲解完了Vue2的Observer的实现,同时讲解了一个简单的例子,对此还有疑问的同学可以在下面评论留言。
!!!写文章的同时也能巩固自己的所学。fighting!!!