组件引用

在 Vitarx 中,你可以通过 ref 属性获取 DOM 元素或组件实例的引用。对于组件,还可以通过 defineExpose 选择性地暴露内部成员给外部使用。

useRef() — 创建引用

useRef 创建一个响应式引用,初始值为 null。当绑定到元素或组件后,引用的 .value 就会指向对应的实例。

typescript
declare function useRef<T>(): ShallowRef<T | null>
  • 泛型 T 传入 DOM 元素类型时,.value 为对应的 DOM 元素
  • 泛型 T 传入组件类型时,.value 为组件的公开实例
tsx
import { useRef } from 'vitarx'

function App() {
  // 创建 DOM 元素引用
  const divRef = useRef<HTMLDivElement>()
  // 创建组件实例引用
  const childRef = useRef()

  return (
    <div>
      <div ref={divRef}>Hello</div>
      <Child ref={childRef} />
    </div>
  )
}

ref 绑定 DOM 元素

ref 绑定到 HTML 元素上,可以在挂载后通过 .value 访问原生 DOM 元素:

tsx
import { useRef, onMounted } from 'vitarx'

function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>()

  onMounted(() => {
    // 挂载后让输入框自动获取焦点
    inputRef.value?.focus()
  })

  return <input ref={inputRef} placeholder="自动聚焦" />
}

ref 绑定组件实例

ref 绑定到组件标签上,可以在挂载后通过 .value 访问组件的公开实例:

tsx
import { useRef, onMounted, defineExpose, ref } from 'vitarx'

// 子组件
function Counter() {
  const count = ref(0)

  const increment = () => {
    count.value++
  }

  const reset = () => {
    count.value = 0
  }

  // 暴露成员给外部 ref
  defineExpose({ count, increment, reset })

  return <div>计数:{count}</div>
}

// 父组件
function App() {
  const counterRef = useRef()

  onMounted(() => {
    // 通过 ref 调用子组件暴露的方法
    counterRef.value?.increment()
  })

  return (
    <div>
      <Counter ref={counterRef} />
      <button
        onClick={() => {
          counterRef.value?.increment()
        }}
      >
        外部增加
      </button>
      <button
        onClick={() => {
          counterRef.value?.reset()
        }}
      >
        外部重置
      </button>
    </div>
  )
}

defineExpose() — 暴露组件内部成员

默认情况下,通过 ref 获取到的组件实例是一个空对象。你需要使用 defineExpose 显式地暴露内部成员,外部才能通过 ref 访问。

typescript
declare function defineExpose<T extends Record<string, any>>(exposed: T): void
  • exposed:要暴露的键值对对象,键为成员名称,值为对应的值或方法
tsx
import { defineExpose, ref } from 'vitarx'

function Editor() {
  const content = ref('')

  const getContent = () => {
    return content.value
  }

  const setContent = (text: string) => {
    content.value = text
  }

  const clear = () => {
    content.value = ''
  }

  // 暴露方法和响应式数据
  defineExpose({
    content,
    getContent,
    setContent,
    clear
  })

  return (
    <textarea
      value={content}
      onInput={(e) => {
        content.value = e.target.value
      }}
    />
  )
}

defineExpose 必须在组件函数的顶层调用。

ref 绑定回调函数

除了传入 useRef() 创建的引用,ref 属性也支持传入一个回调函数。当元素或组件挂载时,回调函数会被调用并传入对应的实例:

tsx
import { onMounted } from 'vitarx'

function App() {
  let divEl: HTMLDivElement | null = null

  return (
    <div
      ref={(el) => {
        // el 就是 DOM 元素
        divEl = el
        console.log('元素已挂载:', el)
      }}
    >
      Hello
    </div>
  )
}

完整示例

下面是一个综合运用 useRefdefineExpose 的完整示例——一个可外部控制的视频播放器组件:

tsx
import { ref, useRef, onMounted, onDispose, defineExpose } from 'vitarx'

// 视频播放器组件
function VideoPlayer(props: { src: string }) {
  const videoRef = useRef<HTMLVideoElement>()
  const isPlaying = ref(false)
  const currentTime = ref(0)
  const duration = ref(0)

  onMounted(() => {
    const video = videoRef.value
    if (!video) return

    video.addEventListener('play', () => {
      isPlaying.value = true
    })
    video.addEventListener('pause', () => {
      isPlaying.value = false
    })
    video.addEventListener('timeupdate', () => {
      currentTime.value = video.currentTime
    })
    video.addEventListener('loadedmetadata', () => {
      duration.value = video.duration
    })
  })

  onDispose(() => {
    // 组件销毁时暂停播放
    videoRef.value?.pause()
  })

  // 暴露控制方法给外部
  defineExpose({
    isPlaying,
    currentTime,
    duration,
    play: () => {
      videoRef.value?.play()
    },
    pause: () => {
      videoRef.value?.pause()
    },
    seek: (time: number) => {
      if (videoRef.value) {
        videoRef.value.currentTime = time
      }
    }
  })

  return (
    <div class="video-player">
      <video ref={videoRef} src={props.src} />
      <div class="controls">
        <button
          onClick={() => {
            videoRef.value?.play()
          }}
        >
          播放
        </button>
        <button
          onClick={() => {
            videoRef.value?.pause()
          }}
        >
          暂停
        </button>
        <span>
          {currentTime} / {duration}
        </span>
      </div>
    </div>
  )
}

// 父组件 — 通过 ref 控制播放器
function App() {
  const playerRef = useRef()

  const handlePlay = () => {
    playerRef.value?.play()
  }

  const handlePause = () => {
    playerRef.value?.pause()
  }

  const handleSeek = () => {
    // 跳转到第 30 秒
    playerRef.value?.seek(30)
  }

  return (
    <div>
      <VideoPlayer ref={playerRef} src="/video.mp4" />
      <div class="external-controls">
        <button onClick={handlePlay}>外部播放</button>
        <button onClick={handlePause}>外部暂停</button>
        <button onClick={handleSeek}>跳转到 30 秒</button>
      </div>
    </div>
  )
}

下一步

  • 双向绑定 — 使用 useModel 实现属性的双向绑定
  • 组件属性 — 了解 ref 作为内置特殊属性的更多用法
  • 生命周期 — 在 onMounted 中安全地访问 ref