Vue源码分析之Watcher(一)

1,315 阅读4分钟

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

前言

在Vue中存在三种Watcher:

  • 负责视图变化的渲染Watcher
  • 负责执行计算属性更新的的computed Watcher
  • 用户通过watcher api自定义的user Watcher

在前边看响应式对象的时候我们知道数据在执行getter逻辑时依赖收集(dep.depend()),执行setter逻辑时派发更新(dep.notify())。

在整个响应式实现的过程中有一个重点是Dep,它是用来管理所有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 (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    ...
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    ...
  }

  /**
   * Add a dependency to this directive.
   */
  addDep (dep: Dep) {
    ...
  }

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    ...
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    ...
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    ...
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    ...
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    ...
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    ...
  }
}

Watcher接收5个参数:

  • vm:Vue实例
  • expOrFn: 监听的数据表达式(可以使用字符串表示一个路径,也可以使用函数返回要监听的数据)
  • cb:回调函数,用于数据变更后去执行
  • options:设置一些配置参数
  • isRenderWatcher:是否是渲染Watcher

在看代码之前,先来看下Watcher类中,部分属性的含义:

参数含义
expression保存expOrFn表达式,用来log输出
getter要监听的数据
deep是否深层次监听对象内部的变化
user是否是用户通过watcher API创建的user Watcher
lazy是否惰性计算,与computed Watcher有关
sync值发生变化后,是否同步执行回调函数。即不需要将该Watcher推入队列,而是直接在当前Tick执行回调。
dirty标记计算属性是否脏了,要被重新计算,与computed Watcher有关
active标记Watcher是否已经从所有订阅的Sub中移除
deps上一次的Dep实例数组
newDeps新添加的Dep实例数组
depIds保存与deps对应的id Set
newDepIds保存与newDeps对应的id Set
getterWatcher的getter函数
before定义钩子函数,数据变化后触发更新前执行(如beforeUpdate钩子的执行就是在渲染Watcher的before函数)

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)
  // 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 // 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 = process.env.NODE_ENV !== 'production'
    ? expOrFn.toString()
    : ''
  // parse expression for getter
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn
  } else {
    this.getter = parsePath(expOrFn)
    if (!this.getter) {
      this.getter = noop
      process.env.NODE_ENV !== 'production' && 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和value两个属性。

getter

从代码里可以看到expOrFn可以是一个字符串,也可以是函数。对于我们开发者来讲,传入的都是字符串;如果是渲染Watcher,则会传入一个函数。

  1. 首先判断要被监听的数据变量expOrFn是不是函数,如果是直接将expOrFn赋值给getter。

  2. 如果不是函数,则调用parsePath函数处理expOrFn字符串。这里主要处理我们平时写的'a.b.c'这种场景

parsePath函数定义在src/core/util/lang.js中:

export 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/

const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\d]`)
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

首先通过正则来判断path的合法性。如果path满足bailRE正则,则直接返回,否则返回一个函数,在获取Watcher值时调用(后边看get函数时具体来看)。

最后回到getter函数,如果getter为undefined,则直接将loop赋值给getter,并且在开发环境抛出异常,告诉开发者Watcher只接收点分割的path,如果想用全部的js语法,可以考虑使用函数。

noop函数定义在src/shared/utils.js

export function noop (a?: any, b?: any, c?: any) {}

value

this.value = this.lazy ? undefined : this.get()

如果lazy值为true,也就是computed Watcher,则将value的值设置为undefined,否则执行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 {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

总体来看get函数的实现就是使用try...catch...finally语句获得value值。

首先调用pushTarget函数,看到这个函数,是不是很眼熟,在前边看Dep class的时候是看到过的。

pushTarget

pushTarget定义在src/core/observer/dep.js

const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

这里是将当前的Watch push到targetStack数组中,并且把Dep.target设置为当前的Watcher

获取value值

接下来调用this.getter获取value值。

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

上边看parsePath函数的时候,我们知道此时的getter函数是通过遍历segments数组来一步步访问path的属性值,每一步都会触发数据的get拦截函数。

具体可以看一个下边这个例子,我把每一步的obj[segments[i]]都具体打出来了:

image.png

traverse

finally语句的逻辑是无论try...catch是否抛出异常都要执行。首先判断deep值,如果为true说明要对watch的值进行深度监听,使用traverse函数来处理。

想一下我们使用deep的场景:

data() {
    return{
        a: {
            b: 'init'
        }
    }
},
watch: {
    a: {
        handler(newVal) {
            console.log(newVal)
        },
        //如果不设置deep值,将不会触发watcher函数
        deep: true
    }
}

看一下traverse函数怎么实现的,traverse函数定义在src/core/observer/traverse.js

const seenObjects = new Set()

export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

traverse函数通过递归调用来深度处理数据,让对象及数组属性的每一层都被依赖收集。

popTarget

popTarget函数定义在src/core/observer/dep.js

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

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方法用来移除无用的Dep。首先遍历deps数组,如果其中项在newDeps不存在,则移除该项。

移除完无用的依赖以后,将newDepIds和newDeps赋值给depIds和deps,然后清空newDepIds和newDeps