Vue3 - watch 和 watchEffect 用法详解,及相关源码解析

4,164 阅读7分钟

Vue3 使用 watchwatchEffect 监听响应式数据

import { watch, watchEffect } from 'vue'

watch 用法

watch 函数可以监听单个或多个数据源,默认是当数据发生变化时才执行回调函数 它是惰性的,如果想在数据初始化时就执行 需设置 { immediate: true }
watch 官方指南

监听单个响应式数据

1. 监听 ref 类型

const times = ref(0)
// 数组
const mediaList = ref([
  { id: 1, url: '/data/6876/fgha.png' },
  { id: 2, url: '/data/2513/abcd.png' },
]) 

watch(times, (newVal, oldVal) => {
  console.log(times)
})

watch(mediaList, (newVal, oldVal) => {})

2. 监听 reactive 类型

对于 reactive 类型,vue3 会自动将 deep 设置为 true,所以在外部额外设置是不会起作用的

const obj = reactive({
  name: 'Jean',
  age: 18
})

watch(obj, (newVal, oldVal) => {
  console.log('监听obj', newVal.age)
})

// 不需要设置deep,设置了也无效!
// watch(obj, (newVal) => {}, { deep: true })

注意,watch 不能直接监听 reactive 对象的属性,比如 watch(obj.name, (newVal, oldVal) => {}) 是错误的!相当于传入一个非响应的数值
有两种方式解决:1只需传入一个函数并返回需要监听的属性,2toRefs 去解构再监听

const pageData = reactive({ current: 1, rows: 15 })

// 1-常用的
watch(() => pageData.current, (newVal, oldVal) => {})

// 2-或用 toRefs 解构为响应的数据
const { current } = toRefs(pageData)
watch(current, (newVal, oldVal) => {})

4. 监听 computed 计算属性

const memberInfo = computed(() => `用户名:${obj.name},年龄:${obj.age}`)

watch(memberInfo, (newVal) => {
  console.log(newVal)
})

5. 监听组件 props

const props = defineProps({
  epuId: { type: String, required: true }
})

watch(() => props.epuId, (newVal) => {})

6. 监听 pinia 中的 state

const store = useGlobalStore()

watch(() => store.userInfo, (newVal) => {
  if (newVal) {
    // do something...
  }
}, { immediate: true })

监听多个响应式数据

对于多个数据源,需要传入数组形式,注意 回调函数的参数 newValue 此时为数组,可以将数据的新值解构出来

watch([num, loading], ([newNum, newLoading], [oldNum, oldLoading]) => {
  console.log(newNum, newLoading)
})

监听对象的多个特定属性,或不同对象的几个属性

watch([
  () => obj1.a, 
  () => obj2.b
], (newValArr) => {
  console.log(...newValArr)
})

数据初始化时 立刻执行

设置 immediatetrue 该数据初始化时就会立即执行。比如我们需要在项目里监听路由变化,在组件初始化时就拿到 route.path

import { useRoute } from 'vue-router'
const route = useRoute()

watch(route, (newVal) => {
  console.log(newVal.path)
}, {
  immediate: true
})

watchEffect 用法

自动收集依赖

不需要指定需要监听的数据,会自动监听回调函数中的响应式数据,初始化时会立刻执行

watchEffect(() => {
  console.log('watchEffect', obj, num)
})

停止监听

const stop = watchEffect(() => {
  console.log('watchEffect', obj, num)
})

const handleSuspend = () => {
  stop() // 停止监听
}

清除函数

比如在组件卸载 unmounted 时解绑事件

watchEffect((onInvalidate) => {
  window.addEventListener('click', handleClick)
  onInvalidate(() => {
    window.removeEventListener('click', handleClick)
  })
})

是不是和 ReactuseEffect 用法很相似呢

import { useEffect } from 'react'

useEffect(() => {
  window.addEventListener('click', handleClick)
  return () => {
    window.removeEventListener('click', handleClick)
  }
}, [])

源码解析

想要更深入理解和使用 watchwatchEffect,我们需要去源码里大概了解实现原理

watch 源码

先来看看 watch 的源码部分,以下代码非完整源码

// apiWatch.ts 
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
  // 在DEV环境,cb不是函数类型就输出提示(省略)
  if (__DEV__ && !isFunction(cb)) {}
  return doWatch(source as any, cb, options)
}

watch 函数接收三个参数:source 监听的数据源,cb 回调函数,options 监听选项设置,函数会执行并返回 doWatch 函数的返回值

watchEffect 相比有以下特点:

  • 惰性执行副作用
  • 更具体说明了应触发侦听器重新运行的状态
  • 访问被侦听状态的旧值和当前值(旧值由闭包缓存)

source

下面是源码中 source 的类型

export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T) // 单个数据源
type MultiWatchSources = (WatchSource<unknown> | object)[] // 多个数据源

可以看出数据源类型支持单个 Ref 实例、响应式对象 ComputedRef 实例 和 返回相同泛型类型的 effect 函数,还支持以数组形式传入多个数据源

cb

上面 cb 类型虽然是 any,但它有自己的特定类型

export type WatchCallback<V = any, OV = any> = (
  value: V,
  oldValue: OV,
  onInvalidate: InvalidateCbRegistrator
) => any

valueoldValue 为新旧值,onInvalidate 为一个清除副作用的函数

let cleanup: () => void
const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
  cleanup = runner.options.onStop = () => {
    callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
  }
}

options

export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
  immediate?: Immediate
  deep?: boolean
}

WatchOptions 继承了 WatchOptionsBase,所以不仅可以传入 immediatedeep 还可以传入 WatchOptionsBase 中的参数控制副作用执行的行为(flushonTrackonTrigger

以下是类型声明

export declare interface WatchOptionsBase extends DebuggerOptions {
  flush?: 'pre' | 'post' | 'sync';
}

export declare interface DebuggerOptions {
  onTrack?: (event: DebuggerEvent) => void;
  onTrigger?: (event: DebuggerEvent) => void;
}
  • immediate 是否立刻执行(默认值为 false
  • deep 是否进行深度监听
  • flush 控制副作用的处理时机,有三个值:sync(同步的)、pre这是默认值,在组件更新前)、post(在组件更新之后)
  • onTrackonTrigger 用于在开发环境下调试监听器行为的相关入口

watchEffect 源码

watchEffectwatch 的衍生,它可以响应式追踪依赖数据,在数据变更时重新执行回调函数

export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle {
  return doWatch(effect, null, options)
}

doWatch

watchwatchEffect 最终都调用了 doWatch 函数,先看下他的函签名

function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
  instance = currentInstance
): WatchStopHandle

相比 watch 多了一个 instance 参数,它的默认值为 currentInstance,可以理解为当前执行这个方法的组件

doWatch 内部逻辑代码贴出来,里面加了比较详细的注释,有兴趣和时间可以看看:

// 这个函数用于提示非法数据源
const warnInvalidSource = (s: unknown) => {
  warn(
    `Invalid watch source: `,
    s,
    `A watch source can only be a getter/effect function, a ref, ` +
    `a reactive object, or an array of these types.`
  )
}

// 定义一个getter函数用来获取要监听的数据
let getter: () => any
// 是否需要强制触发
let forceTrigger = false 

// 当数据源source是ref时
if (isRef(source)) {
  getter = () => (source as Ref).value // 去取对应数据.value
  forceTrigger = !!(source as Ref)._shallow
} 
// 数据源是reactive,去取对应数据
else if (isReactive(source)) {
  getter = () => source
  deep = true // 将deep设为true
} 
// 为数组形式的多数据源时
else if (isArray(source)) {
  getter = () =>
    source.map(s => {
      if (isRef(s)) {
        return s.value
      } else if (isReactive(s)) {
        // traverse会递归去获得reactive对象的所有键值,
        // 因为只要访问一个键就会把这个键依赖收集住,递归把所有键全部都触发依赖收集
        return traverse(s)
      } else if (isFunction(s)) {
        return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
      } else {
        __DEV__ && warnInvalidSource(s)
      }
    })
} 
// 为函数时
else if (isFunction(source)) {
  // 传入getter、effect函数
  if (cb) {
    // 到这里说明是watch
    getter = () =>
      callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
  } else {
    // 到这里说明是watchEffect
    getter = () => {
      // instance是全局变量 为执行这个方法的组件实例,当组件卸载unmounted就终止
      if (instance && instance.isUnmounted) {
        return
      }
      // cleanup是清理函数(它在下面代码中声明)
      if (cleanup) {
        cleanup()
      }
      return callWithErrorHandling(
        source,
        instance,
        ErrorCodes.WATCH_CALLBACK,
        [onInvalidate]
      )
    }
  }
} else {
  // 数据源异常提示
  getter = NOOP
  __DEV__ && warnInvalidSource(source)
}
// deep在reactive数据中会被修改为true
// 因此对于多数据源,reactive数据默认是深度观测的
if (cb && deep) {
  const baseGetter = getter
  getter = () => traverse(baseGetter())
}

// onInvalidate传入的回调函数fn,被注册到当前effect的onStop里,又传入到了cleanup
// 所以调用cleanup就可以调用fn了
let cleanup: () => void
const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
  cleanup = runner.options.onStop = () => {
    callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
  }
}

// in SSR there is no need to setup an actual effect, and it should be noop
// unless it's eager
if (__NODE_JS__ && isInSSRComponentSetup) {
// 服务端渲染的逻辑
}

let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
// 调度方法,在数据源改变后  因为响应式系统的trigger调用,内部会将数据源的新旧值传入cb中调用
const job: SchedulerJob = () => {
  if (!runner.active) {
    return
  }
  if (cb) {
    // watch(source, cb)
    // 调用runner => getter => 数据源的最新值
    const newValue = runner()
    if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
      // cleanup before running cb again
      if (cleanup) {
        cleanup()
      }
      // 将newValue和oldValue作为参数传入cb调用
      callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
        newValue,
        // pass undefined as the old value when it's changed for the first time
        oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
        onInvalidate
      ])
      oldValue = newValue
    }
  } else {
    // watchEffect
    runner()
  }
}

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

let scheduler: ReactiveEffectOptions['scheduler']
// 根据flush模式优化调度方式
if (flush === 'sync') {
  scheduler = job
} else if (flush === 'post') {
  scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
  scheduler = () => {
    if (!instance || instance.isMounted) {
      queuePreFlushCb(job)
    } else {
      // with 'pre' option, the first call must happen before
      // the component is mounted so it is called synchronously.
      job()
    }
  }
}
// 声明effect
const runner = effect(getter, {
  lazy: true,
  onTrack,
  onTrigger,
  scheduler // 依赖被触发时的调度方法
})
// 将effect推入到当前组件effect队列中
recordInstanceBoundEffect(runner, instance)

// 初始时运行
// 是watch时
if (cb) {
  // 因为watch默认为惰性,immediate为true时就立刻调用job
  if (immediate) {
    job()
  } 
  // 进行effect依赖收集
  else {
    oldValue = runner()
  }
} 
// 将effect推入到post队列中,后面的微任务执行会检查post队列
// 如果有任务就执行effect去追踪依赖
else if (flush === 'post') {
  queuePostRenderEffect(runner, instance && instance.suspense)
} 
// 是watchEffect时
else {
  runner()
}

// watchEffect会返回一个函数
return () => {
  // 将effect的active属性设false,再次调用时当active=false时就不会执行回调函数
  stop(runner)
  if (instance) {
    // 从组件effect队列里删除这个effect
    remove(instance.effects!, runner)
  }
}