Vue2源码解读(五)-Watcher && Scheduler

6,576 阅读7分钟

前篇

上一章,我们对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调用生成的watcher实例才有可能是true,此处之所以为有可能,是因为此值是由用户调用watch调用生成的watcher实例才有可能是true**,此处之所以为有可能,是因为此值是由用户调用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!!!