监听器

监听器用于在响应式数据变化时执行副作用,比如发送请求、操作 DOM、打印日志等。本页介绍 watchwatchEffect 的用法。

watch

watch 用于监听特定的响应式数据源,在数据变化时执行回调。

监听 ref

tsx
import { ref, watch, createApp } from 'vitarx'

function App() {
  const count = ref(0)

  watch(count, (newVal, oldVal) => {
    console.log(`count 从 #123;oldVal} 变为 #123;newVal}`)
  })

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => count.value++}>+1</button>
    </div>
  )
}

createApp(App).mount('#app')

监听 getter 函数

当你只想监听对象的某个属性时,使用 getter 函数:

tsx
import { reactive, watch } from 'vitarx'

const state = reactive({ name: '张三', age: 25 })

// 只监听 name 的变化
watch(
  () => state.name,
  (newVal, oldVal) => {
    console.log(`name 从 #123;oldVal} 变为 #123;newVal}`)
  }
)

监听多个数据源

传入数组可以同时监听多个数据源:

tsx
import { ref, watch } from 'vitarx'

const firstName = ref('张')
const lastName = ref('三')

watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`姓名从 #123;oldFirst}#123;oldLast} 变为 #123;newFirst}#123;newLast}`)
})

监听 reactive 对象

直接监听 reactive 对象时,需要配合 deep 选项:

tsx
import { reactive, watch } from 'vitarx'

const state = reactive({ user: { name: '张三' } })

watch(
  state,
  (newVal) => {
    console.log('state 发生了变化')
  },
  { deep: true }
)

watchEffect

watchEffect 会自动追踪内部使用的响应式数据,当这些数据变化时自动重新执行:

tsx
import { ref, watchEffect, createApp } from 'vitarx'

function App() {
  const count = ref(0)

  // 自动追踪 count,count 变化时自动重新执行
  watchEffect(() => {
    console.log(`当前计数: #123;count.value}`)
  })

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => count.value++}>+1</button>
    </div>
  )
}

createApp(App).mount('#app')

与 watch 的区别

特性watchwatchEffect
指定监听源需要显式指定自动追踪
获取旧值✅ 回调提供新旧值❌ 无法获取
懒执行默认不执行,数据变化才执行创建时立即执行一次
适用场景需要知道变化前后的值只关心最新值,执行副作用

onCleanup 清理副作用

在回调中注册清理函数,避免内存泄漏。清理函数会在下次回调执行前或监听器停止时调用:

tsx
import { ref, watch } from 'vitarx'

const id = ref(1)

watch(id, (newId, oldId, onCleanup) => {
  const timer = setTimeout(() => {
    console.log(`加载用户 #123;newId} 的数据`)
  }, 1000)

  // 注册清理函数——下次 id 变化时,清除上一次的定时器
  onCleanup(() => {
    clearTimeout(timer)
    console.log(`清理用户 #123;oldId} 的请求`)
  })
})

watchEffect 同样支持 onCleanup

tsx
watchEffect((onCleanup) => {
  const timer = setInterval(() => {
    console.log('定时检查:', count.value)
  }, 1000)

  onCleanup(() => {
    clearInterval(timer)
  })
})

immediate 选项

watch 默认是懒执行的——只在数据变化时才触发回调。设置 immediate: true 可以在创建时立即执行一次:

tsx
const count = ref(0)

watch(
  count,
  (newVal, oldVal) => {
    console.log(`count: #123;newVal}`)
  },
  { immediate: true }
)
// 立即输出: "count: 0"

deep 选项

当需要深度监听对象内部嵌套属性的变化时,设置 deep: true

tsx
import { reactive, watch } from 'vitarx'

const state = reactive({
  user: {
    profile: {
      age: 25
    }
  }
})

// 深度监听——任何嵌套属性变化都会触发
watch(
  state,
  (newVal) => {
    console.log('state 发生了变化')
  },
  { deep: true }
)

// 修改深层属性也会触发回调
state.user.profile.age = 26

WARNING

深度监听会遍历对象的所有嵌套属性,对大型对象可能产生性能开销。请谨慎使用,只在确实需要时开启。

flush 选项

flush 控制回调的执行时机:

说明
'pre'DOM 更新前执行(默认)
'post'DOM 更新后执行
'sync'同步执行,数据变化后立即执行
tsx
watch(
  count,
  (newVal) => {
    // 在 DOM 更新后执行,可以访问更新后的 DOM
    console.log('DOM 已更新')
  },
  { flush: 'post' }
)

INFO

大多数情况下使用默认的 'pre' 即可。当你需要在回调中访问更新后的 DOM 时,使用 'post''sync' 模式下数据变化会立即同步执行回调,谨慎使用以避免性能问题。

watch vs watchEffect 选择建议

  • 需要新旧值对比 → 用 watch
  • 需要精确控制监听什么 → 用 watch
  • 只关心最新值,执行副作用 → 用 watchEffect
  • 不确定用哪个 → 用 watch,它更明确可控

完整示例

下面是一个搜索防抖的完整示例:

tsx
import { ref, watch, createApp } from 'vitarx'

function App() {
  const keyword = ref('')
  const results = ref<string[]>([])
  const loading = ref(false)

  // 监听搜索关键词,模拟异步搜索
  watch(keyword, (newVal, oldVal, onCleanup) => {
    if (!newVal.trim()) {
      results.value = []
      return
    }

    loading.value = true
    const timer = setTimeout(() => {
      // 模拟搜索结果
      results.value = [`#123;newVal} - 结果1`, `#123;newVal} - 结果2`, `#123;newVal} - 结果3`]
      loading.value = false
    }, 500)

    // 清理上一次的请求
    onCleanup(() => {
      clearTimeout(timer)
      loading.value = false
    })
  })

  return (
    <div style={{ padding: '20px', maxWidth: '400px', margin: '0 auto' }}>
      <h2>搜索示例</h2>
      <input
        value={keyword}
        onInput={(e) => {
          keyword.value = (e.target as HTMLInputElement).value
        }}
        placeholder="输入关键词搜索"
        style={{
          width: '100%',
          padding: '8px 12px',
          fontSize: '14px',
          boxSizing: 'border-box'
        }}
      />

      {loading.value && <p style={{ color: '#999' }}>搜索中...</p>}

      <ul style={{ paddingLeft: '20px' }}>
        {results.value.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  )
}

createApp(App).mount('#app')

API 参考

typescript
// 监听 ref
declare function watch<T extends Ref>(
  source: T,
  callback: (newValue: T['value'], oldValue: T['value'], onCleanup: WatcherOnCleanup) => void,
  options?: WatchOptions
): Watcher

// 监听 getter
declare function watch<T>(
  source: () => T,
  callback: (newValue: T, oldValue: T, onCleanup: WatcherOnCleanup) => void,
  options?: WatchOptions
): Watcher

// 自动追踪副作用
declare function watchEffect(
  effect: (onCleanup: WatcherOnCleanup) => void,
  options?: WatcherOptions
): EffectWatcher

declare interface WatchOptions {
  immediate?: boolean // 是否立即执行,默认 false
  deep?: boolean | number // 是否深度监听,默认 false
  once?: boolean // 是否只执行一次,默认 false
  flush?: 'pre' | 'post' | 'sync' // 执行时机,默认 'pre'
}

下一步