watch侦听属性原理剖析

220 阅读3分钟

继上篇我们分析了computed的实现原理后,计算属性实现原理请猛戳这里,本篇文章我们来分析一下watch侦听属性的实现原理。

watch

应用场景: 适用于观测某个值的变化去完成一段复杂的业务逻辑。

特点:

  1. 本质上是一个user watcher
  2. 使用方式:
// 1.使用watch选项
watch: {
  name(nv, ov) { // 传递函数
    console.log(nv, ov)
  }, 
  age: { // 传递对象
    handler(nv, ov) {
      console.log(nv, ov)
    },
    deep: true,
    immediate: true
  },
  sex: [ // 传递数组
    function handle (nv, ov) {/* ... */}, 
    function handle2 (nv, ov) {/* ... */}
  ],
  job: 'someMethod' // 传递方法名
  hobby: {
    handler: 'someMethod',
    immediate: true
  },
  'province.city': function (nv, ov) { /* ... */ }
},

// 2. vm.$watch(expOrFn, callback, [options])
vm.$watch('a', (newVal, oldVal) => { // 键路径
  console.log(nv, ov)
}, { // 传入配置项
  deep: true,
  immediate: true
})

vm.$watch(() => this.a, (nv, ov) => { // this.a变化后,会执行监听函数
   console.log(nv, ov)
}, {
  deep: true,
  immediate: true
})

3.vm.$watch 返回一个取消观察函数,用来停止触发回调: var unwatch = vm.$watch('a', cb) unwatch() // unwatch执行后取消观察

初始化流程

watch的初始化也是在initState函数中,在initComputed后,会执行initWatch函数。

function initState (vm: Component) {
  ...,
  if (opts.computed) initComputed(vm, opts.computed)
  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 (Array.isArray(handler)) { // handler为数组
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else { // handler为对象或者函数
      createWatcher(vm, key, handler)
    }
  }
}

createWatcher主要做了以下工作:

1.参数序列化: 如果handler是对象,先用options保存住handler,通过handler.handler拿到用户定义的回调函数 如果handler为字符串,会通过vm.字符串获取到定义在vm实例上的methods方法,并赋值给handler

2.调用vm.$watch方法。

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') { // handler是一个字符串的方法
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

$watch方法是定义在Vue原型上的方法,我们来看一下它的实现:

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any, // 用户传递的函数
  options?: Object
): Function {
   const vm: Component = this
   if (isPlainObject(cb)) {
     return createWatcher(vm, expOrFn, cb, options)
   }
   options = options || {}
   options.user = true
   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) //  cb.apply(context, args)
     popTarget()
    }
    return function unwatchFn () {
      watcher.teardown()
    }
 }

由于用户会主动调用vm.$watch,第二个参数cb可能会传递对象,如果是对象,会再次调用createWatcher序列化参数,然后再次调用vm.$watch。拿到options选项,新增了usertrue,实例化 user watcher。如果用户传递了immediate选项,会立刻执行一次回调函数,最后返回了一个卸载watcher的函数,用户执行这个函数,会把user watchersubs集合中移除,从而停止侦听操作。

让我们通过一个demo去看一下user watcher的执行逻辑:

<div id="app">
  <button @click="change">{{ a }}</button> // 点击按钮a的值由{b: 2} -> {b: 3}
</div>
<script>
  const vm = new Vue({
    el: '#app',
    data() {
      return {
        a: {
          b: 2
        }
      }
    },
    watch: {
      a: {
        handler(newVal, oldVal) {
          console.log(newVal, oldVal)
        },
        deep: true
      }
   },
   methods: {
      change() {
        this.a.b = 3
      }
   }
 })
</script>

由于要观测的a的配置项为对象,调用createWatcher时,options保留了这个对象,调用$watch时,为options新增usertrue的属性,然后调用new Watcher去实例化 user watcher,我们看一下实例化user watcher时传入的参数:

30.png

new Watcher(vm, 'a', handler(newVal, oldVal) {console.log(newVal, oldVal)}, {
  handler(newVal, oldVal) {
    console.log(newVal, oldVal)
  },
  deep: true,
  user: true
})

// watcher内部执行逻辑
class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,  // 'a'  
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
      this.vm = vm
      if (options) {
        this.deep = !!options.deep // true
        this.user = !!options.user // true
      }
      this.cb = cb
      this.active = true
      if (typeof expOrFn === 'function') {
        this.getter = expOrFn
      } else { // 由于expOrFn为字符串'a',所以走else逻辑
        this.getter = parsePath(expOrFn)
        
        /**
        this.getter = function (obj) {
          for (let i = 0; i < segments.length; i++) {
            if (!obj) return
            obj = obj[segments[i]] // 实际访问了vm.a
          }
          return obj
        }
        */
      }
      this.value = this.lazy ? undefined : this.get()
  }
  get () {
    pushTarget(this)   // targetStack.push(target)  Dep.target = user watcher
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm) // 让updateComponent函数执行
    } catch() {} finally {
      if (this.deep) { // true
        traverse(value)
      }
      popTarget() 
      ...
    }
    return value
  }
}

function parsePath (path: string): any { // 'a'
  const segments = path.split('.') // ['a']
  return function (obj) { // this.getter会调用该函数,并传入参数vm
    for (let i = 0; i < segments.length; i++) { // 闭包的经典用用
      if (!obj) return
      obj = obj[segments[i]] // 实际访问了vm.a
    }
    return obj
  }
}

parsePath首先会把传入的key通过split拆分为一个数组,然后返回了一个匿名函数,并将其赋值给this.getter,当我们调用watcher.get函数时,实际上会做2件事:

  1. 先把Dep.target指向user watcher,并将其添加到targetStack中。
  2. 执行this.getter.call(vm, vm), 把vm作为参数传入。此时会执行这个匿名函数,通过闭包的形式拿到先前切割好的数组(此例中segments实际是['a']),遍历数组,通过vm去访问数组里每一项(vm.a),由于a是响应式数据,会触发a的依赖收集,此时a对应的subs集合中就收集当前的user watcher作为依赖,并把vm.a的求值结果{b: 2}返回。

由于我们传递了deep属性,执行完this.getter函数后,会触发traverse({b: 2})函数。我们来看一下traverse函数做了那些工作:

function traverse (val: any) { // {b: 2}
  _traverse(val, seenObjects)
}

function _traverse (val: any, seen: SimpleSet) {
  ...,
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val))) {
    return
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val) // ['b']
    i = keys.length // 1
    while (i--) _traverse(val[keys[i]], seen) // 访问'b',触发子对象的依赖收集
  }
}

traverse 的逻辑也很简单,它实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher。在执行了 traverse 后,我们再对 watch 的对象内部任何一个值做修改,也会调用 watcher 的回调函数了。

执行完traverse逻辑后,会执行popTarget函数。该函数会执行targetStack.pop()操作,由于targetStack目前只有一项,执行pop操作后就变为了空数组,然后重置了dep.target的指向(Dep.target = targetStack[targetStack.length - 1])为undefined

初始化完成后会执行挂载($mount)逻辑,此时会实例化render watcher,然后执行render watchergetter方法,也就是我们之前一直介绍的updateComponent方法。先执行pushTarget(targetStack.push(render watcher) Dep.target = render watcher),执行vm_render时,由于render函数中包含有对响应式数据a的访问,此时会触发a的依赖收集,此时a对应的subs列表中会在新增一项为render watcher

// 模板中访问了a
<button @click="change">{{ a }}</button>

// 渲染watcher的getter函数
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

对 deep watcher 的理解非常重要,如果大家观测了一个复杂对象,并且会改变对象内部深层某个值的时候也希望触发回调,一定要设置 deep 为 true,但是因为设置了 deep 后会执行 traverse 函数,会有一定的性能开销,所以一定要根据应用场景权衡是否要开启这个配置。

更新流程

当我们对a做修改后,首先会先触发set拦截,调用dep.notify,拿到收集的subs集合遍历,拿到每个watcher,依次执行watcher.updatequeueWatchernextTick(flushSchedulerQueue)后,最终会执行到watcher.run方法。我们先看一下此时user watcher的执行逻辑:

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)
        }
      }
    }
}

会重新执行watcher.get方法(再次执行this.getter函数),重新去求一个最新的值,如果新旧值不一致,会执行用户传入的cb,并把新旧值传给cb函数。这就是我们观测一个数据,能拿到最新值和上一个值的原因。

执行完user watcher的更新后,继续执行render watcher的更新流程,此时会在次调用updateComponent函数,生成最新的Vnodepatch生成最新的dom

如果我们传递了immediate参数,会立即执行一次cb函数,并把第一次计算的值传入。 最后返回了一个 unwatchFn 方法,它会调用 watcher.teardown 方法去移除这个 watcher

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  if (options.immediate) { // 立即执行一次cb函数
    cb.call(vm, watcher.value)
  }
  return function unwatchFn () {
    watcher.teardown()
  }
}

class Watcher {
  ...,
  teardown () {
    if (this.active) {
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this) // 从subs中移除当前watcher
      }
      this.active = false
    }
  }
}

手绘流程图如下: 43.png

总结: 侦听属性本质是一个user watcher,通过支持传入不同的配置项(deepimmediate),使用起来会更加灵活,至此侦听属性分析完毕。

响应式原理图

38.png