Vue3系列(五)Composition Api之 Watch与WatchEffect(附源码分析)

168 阅读6分钟

watch

监听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。

默认是懒监听的,即仅在侦听源发生变化时才执行回调函数。

第一个参数是监听源

这个来源可以是以下几种:

  • 一个函数,返回一个值
  • 一个 ref
  • 一个响应式对象
  • ...或是由以上类型的值组成的数组

第二个参数是在发生变化时要调用的回调函数

这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。

当监听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。

第三个可选的参数是一个对象

支持以下这些选项:

  • immediate:在监听器创建时立即触发回调。第一次调用时旧值是 undefined
  • deep: 是否开启深度监听。
  • flush:调整回调函数的刷新时机。
  • onTrack / onTrigger:调试监听器的依赖。
flush: 'pre' // 组件更新前的调用
flush: 'post' // 组件更新后的调用
flush: 'sync' // 强制效果始终同步触发(如果有多个属性同时更新,这将导致一些性能和数据一致性的问题)

具体使用

监听一个 getter 函数

const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
     console.log('新值', count);
     console.log('旧值', prevCount);
  } 
)

监听一个 ref

const count = ref(0)
watch(count, (count, prevCount) => { 
  console.log('新值', count);
  console.log('旧值', prevCount);
})

监听一个 reactive

当直接侦听一个响应式对象时,侦听器会自动启用深层模式,所以使用reactive监听深层对象,开启和不开启deep效果一样

import { ref, watch ,reactive} from 'vue'

let message = reactive({
  nav:{
    bar:{
      name:""
    }
  }
})

watch(
  message,
  (newVal, oldVal) => {
    console.log('新值', newVal);
    console.log('旧值', oldVal);
  },
  { deep: true }
)

监听多个值

import { ref, watch ,reactive} from 'vue'
 
let msg1 = ref('msg1')
let msg2 = ref('msg2')
 
watch([msg1, msg2], (newVal, oldVal) => {
    console.log('新值', newVal); // [msg1, msg2]
    console.log('旧值', oldVal); // [prevMsg1, prevMsg2]
})

watchEffect

立即执行传入的函数,同时会响应式追踪其函数内的依赖,并在其依赖变更时重新运行该函数。

第一个参数就是要运行的函数

这个副作用函数的参数也是一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求 (参见下面的示例)。

let msg1 = ref<string>('msg1')
let msg2 = ref<string>('msg2')

watchEffect(() => {
    console.log('msg1', msg1.value);
    console.log('msg2', msg2.value);
})

第二个参数是一个可选的选项

可以用来调整副作用的刷新时机或调试副作用的依赖。

支持以下这些选项:

  • flush:调整回调函数的刷新时机。
  • onTrack / onTrigger:调试监听器的依赖。
flush: 'pre' // 组件更新前的调用
flush: 'post' // 组件更新后的调用
flush: 'sync' // 强制效果始终同步触发(如果有多个属性同时更新,这将导致一些性能和数据一致性的问题)

清除副作用

在触发监听之前会调用一个函数 可以用于处理你的逻辑

import { watchEffect, ref } from 'vue'

 watchEffect((oninvalidate) => {
    
    oninvalidate(()=>{
        // 在触发监听之前会先执行这里的逻辑
    })
})

返回值一个用来停止更新的函数

let msg1 = ref<string>('msg1')
let msg2 = ref<string>('msg2')

const stop =  watchEffect((oninvalidate) => {
  console.log('msg1', msg1.value);
  console.log('msg2', msg2.value);
  oninvalidate(()=>{
 
  })
},{
  flush:"post",
  onTrigger () {

  }
})
stop() // 调用后不再监听

watch 与 watchEffect 源码分析

源码地址: /packages/runtime-core/apiWatch.ts

// 通过函数重载支持多种传参形式
export function watch<
  T extends object,
  Immediate extends Readonly<boolean> = false
>(
  source: T,
  cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
  options?: WatchOptions<Immediate>
): WatchStopHandle

// implementation
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
  // 如果第二个参数传入的不是一个函数,就直接报错
  if (__DEV__ && !isFunction(cb)) {
    warn(
      `\`watch(fn, options?)\` signature has been moved to a separate API. ` +
        `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
        `supports \`watch(source, cb, options?) signature.`
    )
  }
  // 将参数传递给doWatch去做相应的监听
  return doWatch(source as any, cb, options)
}

doWatch

function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
  if (__DEV__ && !cb) {
    // 这里对传递参数进行限制判断,如果不符合要求就报错
    if (immediate !== undefined) {
      // warn......
    }
    if (deep !== undefined) {
      // warn......
    }
  }

  const warnInvalidSource = (s: unknown) => {
    // warn......
  }

  const instance =
    getCurrentScope() === currentInstance?.scope ? currentInstance : null
  // const instance = currentInstance
  let getter: () => any
  let forceTrigger = false // 是否深层次遍历
  let isMultiSource = false

  if (isRef(source)) { // 如果传递是ref对象
    // 创建一个getter函数并读取ref对象的value属性
    getter = () => source.value
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) { // 如果传递是reactive对象 
    // 直接返回一个getter函数 并且设置 deep为true 深度监听
    getter = () => source
    deep = true
  } else if (isArray(source)) { // 如果传递是数组 就遍历数组,处理数组内的ref和reactive
    isMultiSource = true
    forceTrigger = source.some(s => isReactive(s) || isShallow(s))
    getter = () =>
      source.map(s => {
        if (isRef(s)) { // 是ref对象
          return s.value // 直接返回.value
        } else if (isReactive(s)) { // 是一个reactive对象 
          return traverse(s) // 实现了一个递归去遍历reactive对象的属性
        } else if (isFunction(s)) {
          return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
        } else { // 报错,只能监听响应式对象
          __DEV__ && warnInvalidSource(s)
        }
      })
  } else if (isFunction(source)) { // 如果传递是一个函数,判断有没有传入第二个参数
    if (cb) {
      // getter with cb
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
      // no cb -> simple effect 
      // 没有传入回调 相当于watchEffect
      getter = () => {
        if (instance && instance.isUnmounted) {
          return
        }
        if (cleanup) { // 每次执行前判断有没有清理函数,有的话先调用清理函数
          cleanup()
        }
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onCleanup]
        )
      }
    }
  } else {
    getter = NOOP
    __DEV__ && warnInvalidSource(source)
  }

  // 2.x array mutation watch compat 
  // 传入的是一个数组,表示监听多个
  if (__COMPAT__ && cb && !deep) {
    const baseGetter = getter
    getter = () => {
      const val = baseGetter()
      if (
        isArray(val) &&
        checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
      ) {
        traverse(val)
      }
      return val
    }
  }
  // 如果有传入第二个参数且深度监听
  if (cb && deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter()) // 就进行递归
  }

  let cleanup: () => void
  let onCleanup: OnCleanup = (fn: () => void) => {
    cleanup = effect.onStop = () => {
      callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
    }
  }
  // 初始化旧值
  let oldValue: any = isMultiSource
    ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE) 
    : INITIAL_WATCHER_VALUE
  const job: SchedulerJob = () => {
    if (!effect.active) {
      return
    }
    if (cb) {
      // watch(source, cb)
      const newValue = effect.run() 
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? (newValue as any[]).some((v, i) =>
              hasChanged(v, (oldValue as any[])[i])
            )
          : hasChanged(newValue, oldValue)) ||
        (__COMPAT__ &&
          isArray(newValue) &&
          isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
      ) {
        // cleanup before running cb again
        if (cleanup) { // 每次执行前判断有没有清理函数,有的话先调用清理函数
          cleanup()
        }
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          // 第一次执行时 旧值就是undefind或者空数组
          oldValue === INITIAL_WATCHER_VALUE 
            ? undefined
            : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
            ? []
            : oldValue,
          onCleanup
        ])
        oldValue = newValue
      }
    } else {
      effect.run()
    }
  }

  // important: mark the job as a watcher callback so that scheduler knows
  // it is allowed to self-trigger (#1727)
  job.allowRecurse = !!cb

flush

// 使用调度器
let scheduler: EffectScheduler
  if (flush === 'sync') { // 同步执行
    scheduler = job as any // the scheduler function gets called directly
  } else if (flush === 'post') {
    // queuePostRenderEffect 组件更新后执行
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // default: 'pre'
    job.pre = true
    if (instance) job.id = instance.uid
    scheduler = () => queueJob(job)
  }
  // 收集了一下依赖  但没有触发依赖更新
  const effect = new ReactiveEffect(getter, scheduler)

  // 有设置onTrack跟onTrigger调试的话
  if (__DEV__) {
    effect.onTrack = onTrack
    effect.onTrigger = onTrigger
  }

}

更新

// initial run
  if (cb) {
    if (immediate) {
      job() // 如果设置了immediate 立即执行 就立马调用job
    } else {
      oldValue = effect.run() // 把oldValue设置成当前值
    }
  } else if (flush === 'post') {
    queuePostRenderEffect(
      effect.run.bind(effect),
      instance && instance.suspense
    )
  } else {
    effect.run()
  }

  const unwatch = () => {
    effect.stop()
    if (instance && instance.scope) {
      remove(instance.scope.effects!, effect)
    }
  }