news 2026/9/16 16:08:27

Ripple DOM Refs 实战指南:ref 赋值、回调 Ref、多 Ref、组件转发与 createRefKey 的完整解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ripple DOM Refs 实战指南:ref 赋值、回调 Ref、多 Ref、组件转发与 createRefKey 的完整解析

Ripple DOM Refs 实战指南:ref 赋值、回调 Ref、多 Ref、组件转发与 createRefKey 的完整解析

【免费下载链接】ripplethe elegant TypeScript UI framework项目地址: https://gitcode.com/GitHub_Trending/ripple25/ripple

本篇基于 Ripple 官方文档 Referencing DOM Elements 展开,系统讲解 Ripple 中捕获 DOM 节点的完整方案:普通可变标识符 ref、track()产生的Trackedref、回调 ref 及其清理函数、数组形式多 ref、组件转发,以及用于程序化装配 ref 的createRefKey()。读完之后,你不仅会写 ref,还能从源码层面理解 Ripple 是如何在挂载/卸载时赋值与清空 ref、如何在 props 展开(spread)中识别 ref,以及 SSR 场景下的行为差异。

Ref 的基本语法:一个 ref 还是多个 ref

Ripple 中的 ref 使用标准的 JSX 属性形态,用来捕获某个元素背后的 DOM 节点。官方文档给出的两种语法如下:

语法用途
ref={value}为当前元素或组件提供单个 ref。
ref={[a, b]}为同一个元素提供多个 ref。

ref 的值可以是以下几种形态:

  • 回调函数(callback):挂载时接收 DOM 节点,可返回清理函数;
  • track()创建的Tracked:把 DOM 节点存入响应式信号,可在任意响应式作用域中读取;
  • 可变标识符或成员表达式(mutable identifier / member expression):由框架在挂载时赋值、卸载时清空。

官方文档给出的完整示例覆盖了这三种形态:

import { track } from 'ripple'; export default function App() @{ let div: HTMLDivElement | undefined; const input = track<HTMLInputElement | null>(null); const state: { button?: HTMLButtonElement } = {}; <> <div ref={div}>Hello world</div> <input ref={input} type="text" /> <button ref={state.button}>Save</button> </> }

这里div是普通let变量,inputTrackedstate.button是成员表达式。文档明确了生命周期语义:可变 ref 在元素挂载时被赋值,在元素卸载时被清空(clear)

这一语义在测试套件 ref.test.tsrx 中有直接验证,例如“clears a plain let variable via ref={var} when the host element unmounts”用例:用@if (show)条件渲染一个<div ref={div}>,初始断言divHTMLDivElement实例,点击按钮把show置为false后,断言div变为null,确认卸载清空行为成立。

一个值得注意的优先级规则(同样来自该测试文件):当变量同时“看起来像”函数和标识符时,Ripple 走函数路径(function wins over setter);当变量持有Tracked时,走 Tracked 路径(Tracked wins over setter)。也就是说,把track()返回值赋给普通变量再传给ref,Ripple 依然能识别它是Tracked并正确写入。

回调 Ref:挂载赋值、卸载清理、函数工厂

回调 ref 在元素挂载时接收 DOM 节点;如果你在回调中返回一个清理函数,它会在元素被移除时执行。官方文档示例:

export function App() @{ function setup(node: HTMLDivElement) { console.log('mounted', node); return () => { console.log('unmounted', node); }; } <div ref={setup}>Hello world</div> }

回调也可以直接内联在属性位置,顺便完成“赋值 + 日志 + 卸载复位”三件事:

export function App() @{ let div: HTMLDivElement | undefined; <div ref={(node) => { div = node; console.log('mounted', node); return () => { div = undefined; }; }} > Hello world </div> }

回调 ref 还有一个重要应用场景:函数工厂。当第三方库替你返回 ref 回调、或者 ref 的初始化逻辑需要传参配置时,工厂形式非常合适:

import { fadeIn } from 'some-library'; export function App({ ms }) { return <div ref={fadeIn({ ms })}>Hello world</div> }

也就是说,任何“接受配置、返回 ref 回调”的 API(例如动画库、弹窗定位库)都能直接作为ref值使用,而fadeIn({ ms })在运行时求值返回的回调会被框架当作 ref 处理。

多个 Ref:同一个元素挂多个 ref

当一个 DOM 元素需要被多个逻辑同时引用时,ref接受数组形式。官方文档示例中,同一个<input>同时挂了一个普通变量、一个Tracked和一个内联回调:

import { track } from 'ripple'; export function App() @{ let input: HTMLInputElement | undefined; const trackedInput = track<HTMLInputElement | null>(null); <input ref={[input, trackedInput, (node) => console.log(node)]} /> }

数组中每一项都遵循前面所述的 ref 值规则:可变标识符会被赋值/清空,Tracked会写入/清空,回调会被调用(并注册其返回的清理函数)。

组件转发:ref 作为普通 prop 传递

Ripple 没有隐式的forwardRef机制——组件把ref={...}当作一个普通 prop 接收。你要么显式转发它,要么把它包含在展开到宿主元素的那个 spread 里。官方文档的第一个示例用 spread 一把转发所有剩余 props:

function Input({ id, ...rest }) { return <input {id} {...rest} /> } export function App() @{ let input: HTMLInputElement | undefined; <Input id="email" ref={input} /> }

Input...rest(包含ref)整体展开到真正的<input>上,于是外部的 ref 落到了宿主元素。测试套件中“works with spreading from composite component”用例验证了这一点:父组件给复合组件Childref={componentRef}Child内部把...rest展开到<pre>上,最终捕获的节点就是文档里第一个<pre>

另一种更常见的模式是命名 prop:比如把 prop 叫inputRef。文档指出,inputRef这类命名 prop 就是普通的组件 API prop,如果你想让它生效为 ref,需要在接收组件内部把它传给ref={...}

export function Field({ inputRef, ...rest }) { return <label> Search<input type="search" ref={inputRef} {...rest} /> </label> } export function App() @{ let input: HTMLInputElement | undefined; <Field inputRef={input} placeholder="Search docs" /> }

注意这里有两层语义差异,测试文件里都有对应用例:

  • inputRef作为普通 prop 传给组件时,它不会被当作 ref 自动生效,必须由组件内部显式写ref={inputRef}(“forwards a named ref prop explicitly through a component”用例);
  • 但如果组件内部把该 prop 直接展开到宿主元素上(如<input {ref} {...rest} />),运行时能识别出它是 ref 并正确挂载(“forwards an ordinary named prop explicitly from a host spread”用例)。

类型层面,RefValue类型由 Ripple 重新导出(见 types/index.d.ts 中export type { RefValue } from '@tsrx/core/runtime/ref'),测试中用PropsWithExtras<{ input_ref: RefValue<HTMLInputElement> }>给组件 props 标注 ref 成员,这是编写可复用组件时推荐的类型写法。

createRefKey:把 ref 放进任意展开的 props 对象

createRefKey()是 Ripple 提供的一个辅助 API:它创建一个唯一的对象键,当含有该键的对象被展开到元素上时,Ripple 会把对应值识别为 ref。这在 ref 需要程序化装配时特别有用——比如你有一个动态构造的 props 对象,想同时包含idvalue和一个 ref 回调:

import { createRefKey, track } from 'ripple'; export function App() @{ let &[value] = track(''); let input: HTMLInputElement | undefined; const props = { id: 'example', value, [createRefKey()]: (node: HTMLInputElement) => { input = node; const onInput = () => { value = node.value; console.log(value); }; node.addEventListener('input', onInput); return () => { input = undefined; node.removeEventListener('input', onInput); }; }, }; <> <input type="text" {...props} /> <Input {...props} /> </> } function Input({ id, value, ...rest }) { return <input type="text" {id} {value} {...rest} /> }

这个例子里 ref 回调同时做了两件事:把节点存入input变量,并手动绑定input事件监听(卸载时通过返回的清理函数移除监听)。注意这个 props 对象同时被展开到了宿主<input>和复合组件<Input>上——两条路径都能识别 ref,这在测试用例 “should handle spreading into composite refs” 中被验证:断言 ref 回调恰好被调用了两次(host 与 composite 各一次)。

源码层面:ref 键为什么能被识别

从源码结构看,createRefKey()在客户端的实现非常轻量——runtime.js 中它就是一个返回带固定描述的 Symbol 的工厂函数:

export function ref_prop() { return Symbol(REF_PROP); }

REF_PROP常量定义为字符串'ref'(见 constants.js),并在客户端入口以createRefKey的名字导出(index-client.js):

export { ref_prop as createRefKey } from './internal/client/runtime.js';

也就是说,createRefKey()每次返回一个description === 'ref'的全新 Symbol,Spread 更新逻辑据此区分“真正的 ref”和“普通属性”。

识别发生在元素 spread 的处理函数apply_element_spread中(render.js)。其核心机制可以概括为三点:

  1. Symbol 键扫描:遍历 props 对象的所有自有 Symbol,只处理description === 'ref'的键(即createRefKey()生成的键),为每个 ref 建立独立的响应式 effect(create_spread_ref_effect),ref 函数变化或旧 effect 已销毁时才会销毁重建;
  2. 字符串键中的 ref 值检测:对普通字符串键,用is_ref_prop(value)判断值是否为 ref 属性。测试用例 “reports ordinary functions and ref objects as non named-ref props” 明确了边界:普通函数和{ current: null }这类 ref-like 对象都不会被误判为 refisRefProp(() => {})isRefProp({ current: null })均返回false),Ripple 识别的是带标记的 ref 值而非任意函数;
  3. 卸载对称性:当某个 ref 键从 props 中消失时,对应的 effect 被销毁,同时之前为该键注册的事件监听也会被移除(remove_listeners分支)。测试中“clears the Tracked when the host element unmounts”“clears a component ref prop when a host spread changes it to a regular prop”等用例覆盖了 ref 从 spread 中移除、ref 与普通属性互相转换等边角场景,确认赋值与清空是严格配对的。

服务端行为:SSR 下 ref 是 no-op

从源码结构看,ref 是纯客户端概念:服务端运行时把createRefKey导出为noop(index-server.js):

export const createRefKey = noop;

因此在使用 SSR 的项目中,含有createRefKey()的 props 对象在服务端渲染阶段不会造成任何副作用,DOM 节点捕获只在客户端挂载(hydrate/挂载)后发生。写跨端组件时,依赖 ref 的逻辑都应放在回调或effect中,而不是组件顶层同步代码里。

与响应式系统的结合

Ripple 的 ref 与响应式体系是打通的,这也是它与“命令式 ref 对象”方案的主要差异点:

  • Trackedref 让 DOM 节点本身成为响应式数据const input = track<HTMLInputElement | null>(null)之后,任何effect、事件处理函数或渲染表达式都可以读取input的当前值,节点挂载/卸载会自动反映到该信号上(测试 “captures a host element into a Tracked via ref={tracker}” 与 “clears the Tracked when the host element unmounts” 分别验证了写入与清空);
  • 读取时机要注意:组件的 setup 阶段先于 DOM 节点创建,所以在 setup 中同步读取let div仍是undefined,测试中统一用effect(() => { captured = div ?? null; })来观察挂载后的值(见 ref.test.tsrx 中capturing a host element用例的注释说明)。这是一个容易踩的坑:把 DOM 节点存入变量后,请在 effect 或事件回调中读取,不要在组件初始化代码里同步断言它非空

小结

Ripple 的 DOM refs 设计可以归纳为一句话:ref 值就是数据,框架负责赋值与清理的配对。核心要点回顾:

场景写法关键语义
简单捕获ref={div}let变量或成员表达式)挂载赋值,卸载清空
响应式捕获ref={track<T \| null>(null)}写入/清空Tracked,可被响应式系统读取
带清理逻辑ref={(node) => { ...; return cleanup }}挂载调用,卸载执行返回的清理函数
库提供的 refref={fadeIn({ ms })}函数工厂返回 ref 回调,直接可用
一个元素多个 refref={[a, b, cb]}每项独立按规则处理
组件转发<Input ref={x} />+ 组件内{...rest}ref={inputRef}ref 是普通 prop,需显式落到宿主元素
程序化装配props[createRefKey()] = cbspread 时按description === 'ref'的 Symbol 键识别

以上所有行为都可以直接对照 ref.test.tsrx 中的 20 余个测试用例复现验证,识别与 spread 更新的实现细节则集中在 render.js 与 runtime.js,可作为进一步阅读源码的入口。

【免费下载链接】ripplethe elegant TypeScript UI framework项目地址: https://gitcode.com/GitHub_Trending/ripple25/ripple

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 16:08:23

Spring框架核心注解详解与最佳实践

1. Spring框架注解体系概述在Spring框架的实际开发中&#xff0c;注解(Annotation)已经成为现代Java开发的核心工具。相比传统的XML配置方式&#xff0c;注解提供了更直观、更简洁的代码组织方式。我刚开始接触Spring时&#xff0c;面对各种注解也是一头雾水&#xff0c;但随着…

作者头像 李华
网站建设 2026/9/16 16:07:48

5分钟部署免费AI简历编辑器:Magic Resume完整上手实录

5分钟部署免费AI简历编辑器&#xff1a;Magic Resume完整上手实录 【免费下载链接】magic-resume free online AI resume editor&#xff0c;the only official website is https://magicv.art 项目地址: https://gitcode.com/GitHub_Trending/ma/magic-resume Magic Re…

作者头像 李华
网站建设 2026/9/16 16:04:14

Trie树在算法题中的应用与C++实现详解

1. 项目概述&#xff1a;Trie树在算法题中的应用价值前缀树&#xff08;Trie&#xff09;这个数据结构我第一次接触是在处理搜索引擎关键词提示的需求时&#xff0c;后来发现它在算法题中出现的频率越来越高。LeetCode 208题作为Trie的经典实现题目&#xff0c;被纳入了Hot 100…

作者头像 李华
网站建设 2026/9/16 16:03:39

Vue+SpringBoot构建学生综合素质评价系统实战

简介&#xff1a;这是一套面向计算机专业本科生的高分毕业设计级学生综合素质评价系统&#xff0c;采用前后端分离架构&#xff0c;基于Vue实现管理界面、SpringBoot构建后端服务&#xff0c;完整覆盖学生自评、教师评价、教务审核及加权评分等核心业务流程&#xff0c;同时集成…

作者头像 李华