Vue源码之watch

56 阅读3分钟

Vue的watch基本使用

在读watch的源码之前,我们先回顾一下watch的使用方法,可以看下面的这个demo。

<template>
  <div>
    <div>firstName: <input v-model="firstName"></div>
    <div>lastName: <input v-model="lastName"></div>
    <h2>fullName: {{ fullName }}</h2>
    <button @click="changeColor">改变肤色</button>
    <div>肤色:{{ detail.feature.skinColor }}</div>
    <button @click="changeName">改变名字</button>
    <div>名字:{{ detail.name }}</div>
  </div>
</template>
<script>
export default {
  data () {
    return {
      firstName: 'Jack',
      lastName: 'Cheng',
      fullName: '--',
      detail: {
        name: 'sss',
        age: 22,
        like: ['reading', 'exercise'],
        feature: {
          hair: 'long',
          skinColor: 'black'
        }
      }
    }
  },
  methods: {
    changeColor () {
      this.detail.feature.skinColor = 'yellow'
    },
    changeName () {
      this.detail.name = 'hhhh'
    }
  },
  watch: {
    firstName (newVal, oldVal) {
      console.log('firstName: ', newVal, oldVal)
      this.fullName = newVal + ' ' + this.lastName
    },
    lastName: {
      handler (newVal, oldVal) {
        console.log('lastName: ', newVal, oldVal)
        this.fullName = this.firstName + ' ' + newVal
      },
      immediate: true,
    },
    'detail.feature': {
      handler (newVal, oldVal) {
        console.log('我监听到了detail对象的改变', newVal, oldVal)
      },
      deep: true
    },
    'detail.name': [
      function handler1 () {
        console.log('改变了')
      },
      function handler2 () {
        console.log('改变了')
      }
    ]
  }
}
</script>

第一次页面展示效果:

WX20240108-144302@2x.png 由于设置了immediate: true,所以在第一次加载页面时,会执行监听的回调。
点击“改变肤色”,由于该feature属性是对象上的深层属性,所以我们需要加deep: true,可以实现深层监听。

image.png 其实,wach的属性还可以设置数组,只是不经常这样用,但是在watch的实现源码中,对数组这种类型进行了兼容,所以这里加了一个数组的例子。
点击“改变名字”,可以看一下效果。

image.png

watch源码分析

先从入口出发,找到初始化函数initState

export function initState(vm: Component) {
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)

  // Composition API
  initSetup(vm)

  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    const ob = observe((vm._data = {}))
    ob && ob.vmCount++
  }
  if (opts.computed) initComputed(vm, opts.computed)
  // 📢:在这里读取开发者设置的watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initWatch 函数

在这里,遍历了每一个侦听属性,并判断其类型,经过兼容处理,最后都会调用createWatcher这个方法。

function initWatch(vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

createWatcher 函数

由于侦听器的属性值可能为对象,也可能为函数,所以在这里进行了处理。
主要是将执行的回调函数存储在handler中,如果侦听属性为对象,则将这个对象存储在options中,最后将其作为参数,调用$watch方法。

function createWatcher(
  vm: Component,
  expOrFn: string | (() => any),
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

isPlainObject这个函数只是判断值是否为对象数据类型。

export function isPlainObject(obj: any): boolean {
  return _toString.call(obj) === '[object Object]'
}

Vue.prototype.$watch

在这个方法中,主要是创建了watcher实例。

 Vue.prototype.$watch = function (
    expOrFn: string | (() => any),
    cb: any,
    options?: Record<string, any>
  ): Function {
    const vm: Component = this
    // 📢:这里确保cb是个函数
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    // 📢:用户侦听器标识
    options.user = true
    // 📢:每一个侦听属性都生成了一个watcher实例
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // 判断是否为立即执行
    if (options.immediate) {
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    return function unwatchFn() {
      watcher.teardown()
    }
  }

Watcher 函数

这里可以说明一下,在三个地方会创建watcher实例:渲染watcher、计算watcher、用户侦听watcher,这里我们主要分析用户侦听watcher
由于Watcher这个类上面的代码比较多,这里精简化了一下,去掉不用关注的代码,更方便专注watch的实现分析。

export default class Watcher {
  constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function, // 要执行的回调函数
    options?: WatcherOptions | null, // 配置的侦听属性值
    isRenderWatcher?: boolean // 不用关心,渲染watcher用到的
  ) {
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user // 这里为true,标识用户侦听器
      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.post = false
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = __DEV__ ? expOrFn.toString() : ''
    // 📢:这里lazy为 false,所以直接调用get()
    this.value = this.lazy ? undefined : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get() {
    // 📢:这个方法和下面popTarget方法主要是建立依赖关系,实现了监听的数据上绑定我们侦听器的watcher,以及渲染watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 📢:获取监听属性的值
      value = this.getter.call(vm, vm)
    } catch (e: any) {
      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
      // 📢:这里deep为true,可以深层监听,会调用traverse方法,对当前监听的对象或者数组进行递归处理,变为深度监听。
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   */
  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)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps() {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp: any = 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
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown() {
    if (this.vm && !this.vm._isBeingDestroyed) {
      remove(this.vm._scope.effects, this)
    }
    if (this.active) {
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
      if (this.onStop) {
        this.onStop()
      }
    }
  }
}

traverse源码实现了一个递归调用,处理深层监听

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

function _traverse(val: any, seen: SimpleSet) {
  let i, keys
  const isA = isArray(val)
  if (
    (!isA && !isObject(val)) ||
    val.__v_skip /* ReactiveFlags.SKIP */ ||
    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 if (isRef(val)) {
    _traverse(val.value, seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

pushTarget、popTarget 函数

这两个方法维护了一个栈,以及Dep.target的值。
在页面渲染的时候,targetStack = [ 渲染watcher ]Dep.target = 渲染watcher
在读取用户的watch属性时,targetStack = [ 渲染watcher, 用户侦听watcher ]Dep.target = 用户侦听watcher。并将用户侦听watcher收集到监听数据上。
调用popTarget方法,targetStack = [ 渲染watcher ]Dep.target = 渲染watcher,将渲染watcher和数据之间的依赖进行收集。
监听的数据改变,就会触发set方法,同时触发依赖的watcher去执行回调。

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack: Array<DepTarget | null | undefined> = []

export function pushTarget(target?: DepTarget | null) {
  targetStack.push(target)
  Dep.target = target
}

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

可以总结一下,这两个方法其实就是进行了一个依赖收集,再结合发布订阅者模式,当数据变化了,就会通知相关以来的的watcher,从而进行一些操作。