vue2响应式原理(7)-- 侦听属性watch

124 阅读2分钟
const vm = new Vue({
        data: {
          a: 1,
          b: 2,
          c: 3,
          d: 4,
          e: {
            f: {
              g: 5
            }
          }
        },
        watch: {
          a: function (val, oldVal) {
            console.log('new: %s, old: %s', val, oldVal)
          },
          // 方法名
          b: 'someMethod',
          // 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
          c: {
            handler: function (val, oldVal) {
              /* ... */
            },
            deep: true
          },
          // 该回调将会在侦听开始之后被立即调用
          d: {
            handler: 'someMethod',
            immediate: true
          },
          // 你可以传入回调数组,它们会被逐一调用
          e: [
            'handle1',
            function handle2(val, oldVal) {
              /* ... */
            },
            {
              handler: function handle3(val, oldVal) {
                /* ... */
              }
              /* ... */
            }
          ],
          // watch vm.e.f's value: {g: 5}
          'e.f': function (val, oldVal) {
            /* ... */
          }
        }
      })
      vm.a = 2 // => new: 2, old: 1

初始化watch

src/core/instance/state.ts

export function initState(vm: Component) {
  const opts = vm.$options
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

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

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

1、遍历watch对象,或取每一个key对应的值handler,const handler = watch[key],如果是数组,遍历数组,调用 createWatcher(vm, key, handler[i]),不是数组,就直接调用

2、如果handler是对象,做一下处理,将handler赋值给options,handler对象中的handler才是真正的处理函数,如果handler是个字符串,那就是写在methods中的方法名,直接在实例vm上取

3、 最后返回vm.$watch(expOrFn, handler, options)

src/core/instance/state.ts

Vue.prototype.$watch = function (
    expOrFn: string | (() => any),
    cb: any,
    options?: Record<string, any>
  ): 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) {
      pushTarget()
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    return function unwatchFn() {
      watcher.teardown()
    }
  }
}

export function invokeWithErrorHandling(
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    //...
  } catch (e: any) {
    handleError(e, vm, info)
  }
  return res
}

1、用户可以调用vm.$watch,传递的cb可以是对象,也可以是函数,如果传递的cb是对象,就调用createWatcher(vm, expOrFn, cb, options)处理一下

2、options.user = true这是一个user watcher,在Watcher类中处理逻辑有些不一样,如果options.immediate为true,执行pushTarget(),没有传参数 Dep.target = undefinedinvokeWithErrorHandling,立即执行一遍用户的cb函数,并把watcher.value作为参数传给cb,最后再执行popTarget(),将Dep.target还原为上一次的值

3、最后返回一个unwatch函数,调用watcher.teardown(),取消监听

接下来看一下Watcher中user watcher的逻辑

watch收集依赖

export default class Watcher implements DepTarget {
  vm?: Component | null
  expression: string
  cb: Function
  id: number
  deep: boolean
  user: boolean
  lazy: boolean
  sync: boolean
  dirty: boolean
  deps: Array<Dep>
  newDeps: Array<Dep>
  depIds: SimpleSet
  newDepIds: SimpleSet
  getter: Function
  value: any
  post: boolean

  constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
    if ((this.vm = vm) && isRenderWatcher) {
      vm._watcher = this
    }
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } 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()
    if (isFunction(expOrFn)) {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy ? undefined : this.get()
  }

  get() {
    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 {
    
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
}

先判断expOrFn,如果是个函数,直接赋值给getter,this.getter = expOrFn,这里this.lazy = false,所以this.value = this.get(),例如:

// 函数  
vm.$watch(  
  function () {  
  // 表达式 `this.a + this.b` 每次得出一个不同的结果时  
  // 处理函数都会被调用。  
  // 这就像监听一个未被定义的计算属性  
  return this.a + this.b  
},  
  function (newVal, oldVal) {  
  // 做点什么  
  }  
)

在get中,调用this.getter,读取a,b,a,b的dep会收集当前的user watcher,

如果expOrFna.b.c这种字符串,需要parsePath解析,user watcher会被vm.a.b.c的dep收集

watch触发更新

当watch中依赖的数据更新,也是在setter中通过dep.notify(),然后调用收集的各个watcher的update方法,最后还是放到队列中异步更新,前面文章已经分析过了异步更新

update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

最后还是会调用watcher的run方法去更新

run() {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(
            this.cb,
            this.vm,
            [value, oldValue],
            this.vm,
            info
          )
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

在run中调用用户的cb方法,把新值和旧值传递给cb,如果参数中配置sync:true,就直接调用wather的run方法,不在放到队列中去异步更新

watch的deep

      if (this.deep) {
        traverse(value)
      }
       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)) ||
    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)
  }
}