【Vue3源码学习】响应式 api: watchEffect

2023-11-12

在 Vue2中watchoption 写法中一个很常用的选项,使用它可以非常方便的监听一个数据源的变化,而在 Vue3 中watch 独立成了一个 响应式api。

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

watchEffect

由于 watch中的许多行为都与 watchEffect 一致,所以 watchEffect 放在首位讲解,为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect 方法。它立即执行传入的一个函数,同时响应式追踪其依赖,并在以来变更时重新运行该函数。

watchEffect 函数的实现非常简洁:

// 首先来看参数类型
export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void

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

export type WatchStopHandle = () => void

export function watchEffect(
  effect: WatchEffect,  // 接收函数类型的变量,并且在这个函数中会传入 onInvalidate 参数,用以清除副作用
  options?: WatchOptionsBase // 在这个对象中有三个属性,你可以修改 flush 来改变副作用的刷新时机,默认为 pre,当修改为 post 时,就可以在组件更新后触发这个副作用侦听器,改同 sync 会强制同步触发。而 onTrack 和 onTrigger 选项可以用于调试侦听器的行为,并且两个参数只能在开发模式下工作。
): WatchStopHandle {
  return doWatch(effect, null, options)
}

其中 DebuggerOptions位于packages\reactivity\src\effect.ts

export interface DebuggerOptions {
  onTrack?: (event: DebuggerEvent) => void   // 追踪时触发
  onTrigger?: (event: DebuggerEvent) => void // 触发回调时触发
}

参数传入后,函数会执行并返回 doWatch 函数的返回值。由于 watch api 也会调用 doWatch 函数,所以 doWatch 函数的具体逻辑我们会放在最后看。先看 watch api 的函数实现。

watch

watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下这个侦听是惰性的,即只有当被侦听的源发生变化时才执行回调。

watchEffect 相比,watch 有以下不同:

  • 惰性地执行副作用;
  • 更具体地说明应触发侦听器重新运行的状态;
  • 访问被侦听状态的先前值和当前值。

watch 函数的函数有四次重载,这里不做具体分析,这里先看下watch的实现参数,后面将详细分析watch实现的核心doWatch

// overload: array of multiple sources + cb
export function watch<
  T extends MultiWatchSources,
  Immediate extends Readonly<boolean> = false
>(
  sources: [...T],
  cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
  options?: WatchOptions<Immediate>
): WatchStopHandle

// overload: multiple sources w/ `as const`
// watch([foo, bar] as const, () => {})
// somehow [...T] breaks when the type is readonly
export function watch<
  T extends Readonly<MultiWatchSources>,
  Immediate extends Readonly<boolean> = false
>(
  source: T,
  cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
  options?: WatchOptions<Immediate>
): WatchStopHandle

// overload: single source + cb
export function watch<T, Immediate extends Readonly<boolean> = false>(
  source: WatchSource<T>,
  cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
  options?: WatchOptions<Immediate>
): WatchStopHandle

// overload: watching reactive object w/ cb
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.`
    )
  }
  return doWatch(source as any, cb, options)
}

watch 接收 3 个参数,source 侦听的数据源,cb 回调函数,options 侦听选项。

source 参数

source 的类型如下:

export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
type MultiWatchSources = (WatchSource<unknown> | object)[]

从两个类型定义看出,数据源支持传入单个的 RefComputed 响应式对象,或者传入一个返回相同泛型类型的函数,以及 source 支持传入数组,以便能同时监听多个数据源。

cb 参数

在这个最通用的声明中,cb 的类型是 any,但 cb 这个回调函数也有类型:

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

在回调函数中,会提供最新的 value、旧 value,以及 onInvalidate 函数用以清除副作用。

options

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

可以看到 options 的类型 WatchOptions 继承了 WatchOptionsBase

分析完参数后,可以看到函数体内的逻辑与 watchEffect 几乎一致,但是多了在开发环境下检测回调函数是否是函数类型,如果回调函数不是函数,就会报警。执行 doWatch 时的传参与 watchEffect 相比,多了第二个参数回调函数。

doWatch

`watchEffect、watch 还是组件内的 watch 选项,在执行时最终调用的都是 doWatch 中的逻辑。

先从 doWatch 的函数签名看起,与 watch 一致。

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

doWatch 函数主要分为以下几个部分:

  1. 标准化 source,组装成为 getter 函数
  2. 组装 job 函数。判断侦听的值是否有变化,有变化则执行 getter 函数和 cb 回调
  3. 组装 scheduler 函数,scheduler 负责在合适的时机调用 job 函数(根据 options.flush,即副作用刷新的时机),默认在组件更新前执行
  4. 开启侦听
  5. 返回停止侦听函数
function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
 
  // 1. 根据 source 的类型组装 getter    
  let getter: () => any 
  if (isRef(source)) {
    getter = ...
  } else if (isReactive(source)) {
    getter = ...
  } else if (isArray(source)) {
    isMultiSource = true
    forceTrigger = source.some(isReactive) 
     getter = ...
  } else if (isFunction(source)) {
    if (cb) {
      getter = ...
    } else {
      getter = ...
    }
  } else {
    getter = ...
    __DEV__ && warnInvalidSource(source)
  }

  // 2. 组装 job
  const job: SchedulerJob = () => {
	// ...
  }

  // 3. 组装 scheduler
  let scheduler: EffectScheduler = ...

  // 4. 开启侦听,侦听的是 getter 函数
  const effect = new ReactiveEffect(getter, scheduler)
  effect.run()

  // 5. 返回停止侦听函数
  return () => {
    effect.stop()
    if (instance && instance.scope) {
      remove(instance.scope.effects!, effect)
    }
  }
}

1. 标准化 source,组装成为 getter 函数

判断 source 的类型,source 的不同类型,标准化包装成 getter 函数:

  • ref:() => source.value
  • reactive:() => traverse(source)
  • 数组:分别根据子元素类型,包装成 getter 函数
  • 函数:用 callWithErrorHandling 包装,实际上就是直接调用 source 函数

相关代码及注释如下:

  const instance = currentInstance // 取当前组件实例
  let getter: () => any            // getter 最终会当做副作用的函数参数传入
  let forceTrigger = false         // 标识是否需要强制更新
  let isMultiSource = false        // 标记传入的是单个数据源还是以数组形式传入的多个数据源。

  // 1. ref 类型 
  if (isRef(source)) {
    getter = () => source.value      // 直接解包取source.value 值,
    forceTrigger = !!source._shallow // 标记会根据是否是shallowRef设置
  // 2. reactive 类型
  } else if (isReactive(source)) {
    getter = () => source  // 直接返回 source,因为 reactive 的值不需要解包获取
    deep = true            // 由于 reactive 中往往有多个属性,所以将deep设置为 true。这里可以看出从外部给 reactive 设置 deep 是无效的
  // 3. 数组 array 类型
  } else if (isArray(source)) {
    isMultiSource = true
    forceTrigger = source.some(isReactive) // 会根据数组中是否存在 reactive 响应式对象来设置
    // 数组形式,由source内各个元素的单个 getter 结果
    getter = () =>
      source.map(s => {
        if (isRef(s)) {
          return s.value
        } else if (isReactive(s)) {
          return traverse(s)
        } else if (isFunction(s)) {
          return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
        } else {
          __DEV__ && warnInvalidSource(s)
        }
      })
   // 4. source 是函数 function 类型
  } else if (isFunction(source)) {
    // 4.1 如果有回调函数
    if (cb) {
      // getter with cb
      // getter 就是 source 函数执行的结果,这种情况一般是 watch api 中的数据源以函数的形式传入。
      // callWithErrorHandling 中做了一些 vue 错误信息的统一处理,有更好的错误提示
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
      // no cb -> simple effect
      // 4.2 如果没有回调函数,那么此时就是 watchEffect api 的场景了
      getter = () => {
        // 如果组件实例已经卸载,则不执行,直接返回
        if (instance && instance.isUnmounted) {
          return
        }
        // cleanup不为void时 清除依赖
        if (cleanup) {
          cleanup()
        }
        // 执行source函数
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onInvalidate]
        )
      }
    }
  // 5. 其余情况 将 getter 设置为空函数,并且报出 source 不合法的警告
  } else {
    getter = NOOP
    __DEV__ && warnInvalidSource(source)
  }

接着会处理 watch 中的场景,当有回调,并且 deep 选项为 true 时,将使用 traverse 来包裹getter 函数,对数据源中的每个属性递归遍历进行监听。

if (cb && deep) {
  const baseGetter = getter
  getter = () => traverse(baseGetter())
}

traverse

traverse 的作用:对于 reactive 对象或设置了参数 deep,需要侦听到深层次的变化,这需要深度遍历整个对象,深层次的访问其所有的响应式变量,并收集依赖。

// 深度遍历对象,只是访问响应式变量,不做任何处理
// 访问就会触发响应式变量的 getter,从而触发依赖收集
export function traverse(value: unknown, seen?: Set<unknown>) {
  if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
    return value
  }
  seen = seen || new Set()
  if (seen.has(value)) {
    return value
  }
  seen.add(value)
  if (isRef(value)) {
    traverse(value.value, seen)
  } else if (isArray(value)) {
    // 继续深入遍历数组
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], seen)
    }
  } else if (isSet(value) || isMap(value)) {
    value.forEach((v: any) => {
      traverse(v, seen)
    })
  } else if (isPlainObject(value)) {
    // 是对象则继续深入遍历
    for (const key in value) {
      traverse((value as any)[key], seen)
    }
  }
  return value
}

之后会声明 cleanuponInvalidate 函数,并在 onInvalidate 函数的执行过程中给 cleanup 函数赋值,当副作用函数执行一些异步的副作用,这些响应需要在其失效时清除,所以侦听副作用传入的函数可以接收一个 onInvalidate 函数作为入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:

  • 副作用即将重新执行时。
  • 侦听器被停止(如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时)。
let cleanup: () => void
let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
  cleanup = runner.options.onStop = () => {
    callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
  }
}

2. 组装 job

Job 函数在 scheduler 函数中被直接或间接调用。

声明一个 job 函数,这个函数最终会作为调度器中的回调函数传入,由于是一个闭包形式依赖外部作用域中的许多变量。

根据是否有回调函数,设置 joballowRecurse 属性,这个设置很重要,能够让 job 作为一个观察者的回调这样调度器就能知道它允许调用自身。

  // 初始化 oldValue
  let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
  // 声明一个 job 调度器任务
  const job: SchedulerJob = () => {
    if (!effect.active) { // 如果副作用以停用则直接返回
      return
    }
    if (cb) {
      // watch(source, cb)
      //  在 scheduler 中需要手动直接执行 effect.run,这里会执行 getter 函数
      // 先执行 getter 获取返回值,如果返回值变化,才执行 cb。
      const newValue = effect.run()
      
      // 判断是否需要执行 cb
      // 1. getter 函数的值被改变,没有发生改变则不执行 cb 回调
      // 2. 设置了 deep 深度监听
      // 3. forceTrigger 为 true
      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()
        }
        // 触发 watch api 的回调,并将 newValue、oldValue、onInvalidate 传入
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          // 首次调用时,将 oldValue 的值设置为 undefined
          oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
          onInvalidate
        ])
        oldValue = newValue // 触发回调后,更新 oldValue
      }
    } else {
      // watchEffect的场景,直接执行 runner
      effect.run()
    }
  }
  // important: mark the job as a watcher callback so that scheduler knows
// 重要:让调度器任务作为侦听器的回调以至于调度器能知道它可以被允许自己派发更新
// it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb

3. 组装 scheduler

getter 中侦听的响应式变量发生改变时,就会执行 scheduler 函数。

声明一个 scheduler 的调度器对象,并根据 flush的传参来确定调度器的执行时机。这一部分逻辑的源码及注释如下:


let scheduler: ReactiveEffectOptions['scheduler'] // 声明一个调度器
if (flush === 'sync') { // 同步
  scheduler = job as any // 这个调度器函数会立即被执行
} else if (flush === 'post') { // 延迟
  // 调度器会将任务推入一个延迟执行的队列中,在组件被挂载后、更新的生命周期中执行。
  scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
    // 默认情况 'pre'
  scheduler = () => {
    // 区分组件是否已经挂载  
    if (!instance || instance.isMounted) {
      queuePreFlushCb(job) // 组件挂载后则会被推入优先执行时机的队列中
    } else {
      // 在 pre 选型中,第一次调用必须发生在组件挂载之前
      // 所以这次调用是同步的
      job()
    }
  }
}

4. 开启侦听

在处理完以上的调度器部分后,会开始创建const effect = new ReactiveEffect(getter, scheduler)对象,并首次执行副作用函数。这里会立即调用 getter 函数,进行依赖收集。

如果依赖有变化,则执行 scheduler 函数:

  • 如果 watch 有回调函数
    • 如果 watch 设置了 immediate 选项,则立即执行 job 调度器任务。
    • 否则首次执行 effect.run()副作用,并将返回值赋值给 oldValue
  • 如果 flush 的刷新时机是 post,则将 effect.run()放入延迟时机的队列中,等待组件挂载后执行。
  • 其余情况都直接首次执行effect.run()副作用。
  const effect = new ReactiveEffect(getter, scheduler)

  if (__DEV__) {
    effect.onTrack = onTrack
    effect.onTrigger = onTrigger
  }

  // initial run
  if (cb) {
    if (immediate) {
      job()  // 有回调函数且是 imeediate 选项的立即执行调度器任务
    } else {
      oldValue = effect.run() // 否则执行一次effect.run(),并将返回值赋值给 oldValue
    }
  } else if (flush === 'post') {
    // 如果调用时机为 post,则推入延迟执行队列
    queuePostRenderEffect(
      effect.run.bind(effect),
      instance && instance.suspense
    )
  } else {
    // 其余情况立即首次执行副作用
    effect.run()
  }

5. 返回停止监听函数

最后 doWatch 函数会返回一个函数,这个函数的作用是停止侦听,所以大家在使用时可以显式的为 watchwatchEffect 调用返回值以停止侦听。

// 返回一个函数,用以显式的结束侦听
return () => {
    effect.stop()
    // 移除当前组件上的对应的 effect
    if (instance && instance.scope) {
      remove(instance.scope.effects!, effect)
    }
}

doWatch 函数到这里就全部运行完毕了,现在所有的变量已经声明完毕。

结语

如果本文对你有一丁点帮助,点个赞支持一下吧,感谢感谢

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Vue3源码学习】响应式 api: watchEffect 的相关文章

随机推荐

  • 蓝牙模块怎么使用_有线音箱完美升级蓝牙功能,只需2杯咖啡钱!

    文章作者 噩梦飘雷结束一天繁忙的工作 相信很多人都跟小值一样 喜欢打开手机 播放起自己喜欢的歌单 音乐响起 就能双脚离地 暂时漂浮于眼前的苟且之上 让自己喘口气 当然比起戴上耳机安静地狂欢 外放音乐 让音符充满房间 是种更身临其境的享受 在
  • 【PAT甲级】1074 Reversing Linked List (25 point(s))

    Given a constant K K K and a singly linked list L L L you are supposed to reverse the links of every
  • 【ESP32-CAM】使用opencv获取ESP32-CAM视频流,并将图像保存至TF卡(一)

    VSCode python opencv ESP32 CAM 本项目仅作为学习记录 不定时更新 Arduino 对于ESP32 CAM 我们使用Arduino来开发 首先需要准备一些硬件 ESP32 CAM 在淘宝大约30rmb一个 烧录底
  • 回归预测算法比较

    GBDT和时间序列分析的区别 GBDT算法做预测分析时考虑到一些影响目标值的因素 而时间序列主要考虑到数据在时序上的一些规律 忽略了其他因素对目标值的影响 这两种算法在实际应用中刚好互补 如果原始数据中包含足够的数据特征 通常情况下使用GB
  • FaceForensics和FaceForensics++【参考文献30和31】

    30 FaceForensics学术界最大的合成视频数据库之一 介绍了一个新的面部操作数据集 大约有50万张经过编辑的图像 来自1000多个视频 这些操作是用最先进的面部编辑方法生成的 它超过了所有现有的视频操作数据集至少一个数量级 利用我
  • 今天,大语言模型革新百度搜索

    机器之心原创 作者 泽南 别搜关键词了 百度搜索直接给你正确答案 最近几天 一些最常用的工具正在被大厂重新发明 ChatGPT 横空出世后 整个科技领域突然就开上了快车道 今年 2 月 微软推出的 AI 版必应 用最先进的大语言模型重构了搜
  • C语言之指针的概念必备练习题

    对于指针 想必是多少人刚开始学习时候的梦魇 笔者也一样 一套关于指针概念的练习题请大家多多仔细阅读 体会不一样的感觉 1 作业标题 711 关于指针的概念 错误的是 作业内容 A 指针是变量 用来存放地址 B 指针变量中存的有效地址可以唯一
  • 用MOS管驱动电机吧

    图示为我应用于实际的空心杯电机驱动电路 可见 该电路通过MCU的PWM信号控制电机 实现了无刷电机的无极调速 该电路的基本思想就是通过PWM信号控制MOS管 SI2302 的开与关 改变PWM的占空比 进而改变流过电机的有效电流 从而控制电
  • python thread_Python thread模块用法详解

    该包在 Python 2 中属于正常可用状态 但在 Python 3 中处于即将废弃的状态 虽然还可以用 但包名被改为 thread 使用 thread 包首先要引入该包 在 Python 2 中使用下面的语句来引入 import thre
  • 关于Python脚本开头两行的:#!/usr/bin/python和# -*- coding: utf-8 -*-的作用 – 指定

    usr bin python 是用来说明脚本语言是python的 是要用 usr bin下面的程序 工具 python 这个解释器 来解释python脚本 来运行python脚本的 coding utf 8 是用来指定文件编码为utf 8的
  • 大数据24小时:京东启动“人工智能加速器”项目,网传王劲因“家庭原因”离开景驰

    数据猿导读 王劲因 家庭原因 离开景驰 原CTO韩旭接任CEO一职 加强区块链布局 京东启动 人工智能加速器 项目 美国云服务商GTT收购欧洲最大云平台营运商 交易金额达23亿美元 以下为您奉上更多大数据热点事件 编辑 abby 官网 ww
  • Eclipse上Maven环境配置使用 (全)

    Eclipse上Maven环境配置使用 全 1 安装配置Maven 1 1 从Apache网站 http maven apache org 下载并且解压缩安装Apache Maven Maven下载地址 http maven apache
  • 30个适合女生玩的可爱网站

    ugmbbc发布于 2008 03 20 13 30 12 2905 次阅读 字体 大 小 打印预览 感谢不要笑我的投递这次推荐给大家的都是非常好玩和可爱的网站 他们都拥有不错的技术和创意 这些网站尤其适合女孩子玩 当然cnBeta是一个罗
  • 河南省计算机考试题目,计算机二级考试模拟题型

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 21 若不想修改数据库文件中的数据库对象 打开数据库文件时要选择 B A 以独占方式打开 B 以只读方式打开 C 以共享方式打开 D 打开 22 某文本型字段的值只能为字母且长度为6 则可将该字
  • 深度学习的一些教程

    几个不错的深度学习教程 基本都有视频和演讲稿 附两篇综述文章和一副漫画 还有一些以后补充 Jeff Dean 2013 Stanford http i stanford edu infoseminar dean pdf 一个对DL能干什么的
  • web自动点击遍历(入口检查)

    目录 web自动点击遍历 入口检查 一 背景 二 技术方案 三 核心代码 3 1代码结构 3 2用例模块 3 3获取HTML代码 3 4正则查找a标签 3 5遍历存储 3 6递归遍历 web自动点击遍历 入口检查 导语 web自动化的存在问
  • Qt debug版本运行正常release版本运行崩溃问题记录

    问题由来 某一项目debug版本运行正常 进入发布阶段 结果release后的版本出现了崩溃问题 因为是release版本 不能debug运行 只能通过printf debug 虽然问题原因很简单 但是耗费了大量时间 故做此记录 以供后续参
  • 网络安全(黑客)自学笔记&学习路线

    谈起黑客 可能各位都会想到 盗号 其实不尽然 黑客是一群喜爱研究技术的群体 在黑客圈中 一般分为三大圈 娱乐圈 技术圈 职业圈 娱乐圈 主要是初中生和高中生较多 玩网恋 人气 空间 建站收徒玩赚钱 技术高的也是有的 只是很少见 技术圈 这个
  • Jeesite4本地及服务器上传文件、图片详解

    大家过年好 你们的老朋友小Q又回来了 最近一段时间忙于公司项目开发 又加上过年比较忙 所以没能留出时间更新内容 大家是不是有点着急了 公司最近在使用jeesite4开发项目 我呢 对这个框架一点也不熟悉 遇到问题都是现查现用 这不 在上传文
  • 【Vue3源码学习】响应式 api: watchEffect

    在 Vue2中watch 是 option 写法中一个很常用的选项 使用它可以非常方便的监听一个数据源的变化 而在 Vue3 中watch 独立成了一个 响应式api 源码地址 packages runtime core src apiWa