从零到一打造 Vue3 响应式系统 Day 24 - Watch:Options

Watch 常用的选项有:

  • immediate:初始化立即执行一次
  • deep:深度监听
  • once:只执行一次,执行后自动停止监听

我们先写一个函数,接受三个参数,默认值是空对象。

jsx 复制代码
export function watch(source, cb, options) {
  const { immediate, once, deep } = options || {}
  ...
}

immediate

immediatetrue 时,watch 会在初始化时立即执行一次 job,此时 callback 中的 oldValueundefined

如果 immediatefalse(或未提供),则初始化阶段只会执行 effect.run() 收集依赖并拿到 oldValue,但不会触发回调。

scss 复制代码
export function watch(source, cb, options) {
  const { immediate, once, deep } = options || {}
  ...
  if(immediate) {
    job() // 第一次立即执行
  } else {
    oldValue = effect.run() // 只收集依赖,记录初始值
  }
  ...
}

once

要实现 once 功能,可以对用户的 callback 做包装:

先缓存原始 callback,再用一个匿名函数替换掉 cb,执行完后立刻调用 stop() 停止监听。

scss 复制代码
export function watch(source, cb, options) {
  const { immediate, once, deep } = options || {}

  if(once) {
    const _cb = cb
    cb = (...args) => {
      _cb(...args)
      stop()
    }
  }
  ...
}

deep

深度监听(deep: true)的原理:在依赖收集阶段,递归访问被监听对象的所有嵌套属性。

访问时会触发 getter,把所有属性都收集为依赖,一旦任意深层属性变化,watch 就能收到通知。

javascript 复制代码
import { isObject } from '@vue/shared'

export function watch(source, cb, options) {
  const { immediate, once, deep } = options || {}
  ...
  if(deep){
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }
}

function traverse(value) {
  if(!isObject(value)) return
  for(const key in value) {
    traverse(value[key])
  }
  return value
}

不过这样可能会遇到循环引用,所以要加一个 Set 来记录访问过的对象:

scss 复制代码
function traverse(value, seen = new Set()) {
  if(!isObject(value)) return value
  if(seen.has(value)) return value // 避免循环引用

  seen.add(value)
  for(const key in value) {
    traverse(value[key], seen)
  }
  return value
}

这样就能避免递归爆栈。


Vue 3.5 对 deep 新增了层级控制 功能:可以用数字指定监听深度。

比如下面例子:

xml 复制代码
<body>
  <div id="app"></div>
  <script type="module">
    import { ref, watch } from '../dist/reactivity.esm.js'

    const state = ref({
      a: {
        b: 1,
        c: {
          d: 1
        }
      }
    })

    watch(state, (newVal, oldVal) => {
      console.log('newVal, oldVal', newVal, oldVal)
    }, { deep: 2 })

    setTimeout(() => {
      state.value.a.c.d = 2
      console.log('更新了')
    }, 1000)
  </script>
</body>

deep 是数字时,代表递归的层级深度:

  • deep: 2 → 监听到第二层,修改 a.b 会触发
  • 修改更深的 a.c.d 不会触发

官方源码里也是这样,超出层级就不会输出:

实现:

scss 复制代码
if(deep){
  const baseGetter = getter
  const depth = deep === true ? Infinity : deep
  getter = () => traverse(baseGetter(), depth)
}

function traverse(value, depth = Infinity, seen = new Set()) {
  if(!isObject(value) || depth <= 0) return value
  if(seen.has(value)) return value

  seen.add(value)
  depth--

  for(const key in value) {
    traverse(value[key], depth, seen)
  }
  return value
}

通过 depth-- 控制递归深度。


reactive 与 function 处理

当我们把 source 改成 reactive 对象时,控制台会报错:

查看官方源码:当监听源是 reactive 时,deep 默认就是 true

所以要调整 getter 初始化逻辑:

  • 如果是 ref → getter = () => source.value
  • 如果是 reactive → getter = () => source,并且默认 deep = true
  • 如果是函数 → 直接作为 getter
scss 复制代码
if(isRef(source)) {
  getter = () => source.value
}else if(isReactive(source)){
  getter = () => source
  if(!deep) deep = true
}else if(isFunction(source)){
  getter = source
}

这样就能兼容 refreactive 和函数。


总结

我们给 watch 增加了三个选项:

  • immediate:初始化立即执行一次
  • once:只执行一次后自动停止
  • deep:支持深度监听,支持层级递归控制,并解决循环引用问题

同时调整了 getter 初始化逻辑,使其兼容 refreactive、函数三种情况。

这样 watch 的基础功能和常用配置就完成了


想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。

相关推荐
飞翔的佩奇2 分钟前
【完整源码+数据集+部署教程】【运动的&足球】足球比赛分析系统源码&数据集全套:改进yolo11-RFAConv
前端·python·yolo·计算机视觉·数据集·yolo11·足球比赛分析系统
支付宝体验科技5 分钟前
SEE Conf 2025:开启体验科技的新十年
前端
TeamDev6 分钟前
使用 Shadcn UI 构建 C# 桌面应用
前端·后端·.net
尘世中一位迷途小书童33 分钟前
Vuetify Admin 后台管理系统
前端·前端框架·开源
参宿733 分钟前
图解Vue3 响应式,手动实现核心原理
前端·javascript·vue.js
2301_8012522240 分钟前
前端框架Vue(Vue 的挂载点与 data 数据对象)
java·前端·javascript·vue.js·前端框架
资讯第一线1 小时前
《Windows Server 2022》 [2025年10月版 ] [官方IOS] 下载
前端
非凡ghost1 小时前
EaseUS Fixo(易我视频照片修复)
前端·javascript·后端
非凡ghost1 小时前
Avast Cleanup安卓版(手机清理优化)
前端·javascript·后端
豆苗学前端1 小时前
长时间不操作自动退出登录(系统非活跃状态下自动登出机制的企业级设计方案)
前端·后端·面试