前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >脱围:使用 ref 保存值及操作DOM

脱围:使用 ref 保存值及操作DOM

作者头像
奋飛
发布2024-05-25 19:25:13
990
发布2024-05-25 19:25:13
举报
文章被收录于专栏:Super 前端

♻️ 前面多篇文章中提及:state 可以 ① 保存渲染间的数据; ② state setter 函数更新变量会触发 React 重新渲染组件。

代码语言:javascript
复制
// 子组件:显示当前时间
function Time() {
    return (
        <p>{new Date().toLocaleString()}</p>
    )
}

export default () => {
    const [counter, setCounter] = useState(0);
    return (<>
        <span>{counter}</span>
        <button onClick={() => setCounter(counter + 1)}>+</button>
        <Time />
    </>)
}

默认情况下,当一个组件重新渲染时,React 会递归地重新渲染它的所有子组件

每一次点击按钮, counter + 1 ,都会导致整个组件渲染(包括 <Time>),因此总是显示当前时间。

如何使得 state 每次加 1,但子组件 <Time> 不变 ?

方式一:子组件使用 state

该方式:只修改子组件

代码语言:javascript
复制
function Time() {
    let [time, setTime] = useState(new Date().toLocaleString());
    return (
        <p>{time}</p>
    )
}

点击按钮,counter + 1,但 <Time> 组件不被重新渲染,保持第一次的值。

⚠️ 相同位置的相同组件会使得 state 被保留下来! 具体可见「续篇:展开聊下 state 与 渲染树中位置的关系

方式二:子组件使用 memo 包裹

该方式:只修改子组件

代码语言:javascript
复制
const Time = memo(() => {
    return (
        <p>{new Date().toLocaleString()}</p>
    )
})

实现效果同上述「方式一」。

通过此更改, <Time> 的所有 props 都与上次渲染时相同(这里都为空), <Time> 跳过重新渲染。关于useMemo 可参阅官网 1

⚓ 方式三:父组件使用 ref

该方式:只修改父组件

代码语言:javascript
复制
export default () => {
    const counterRef = useRef(0);

    return (<>
        <span>{counterRef.current}</span>
        <button onClick={() => counterRef.current = counterRef.current + 1}>+</button>
        <p>{new Date().toLocaleString()}</p>
    </>)
}

同上述「方式一」&「方式二」的差异:当前DOM不发生任何变化(依然为0,其 counterRef.current 的值已经变成了 1)。

当希望组件“记住”数据,又不想触发新的渲染时,便可以使用 ref

ref 是一种脱围机制2,用于保留不用于渲染的值:有些组件可能需要控制和同步 React 之外的系统。

例如,可能需要使用浏览器 API 聚焦输入框,或者在没有 React 的情况下实现视频播放器,或者连接并监听远程服务器的消息。

ref 与 state 不同之处

✈️ 与 state 一样,React 会在每次重新渲染之间保留 ref。但是,设置 state 会重新渲染组件,更改 ref 不会 !

ref

state

useRef(initialValue)返回 { current: initialValue }

useState(initialValue) 返回 state 变量的当前值和一个 state 设置函数 ( [value, setValue])

更改时不会触发重新渲染

更改时触发重新渲染。

可变 —— 可以在渲染过程之外修改和更新 current 的值。

“不可变” —— 必须使用 state 设置函数来修改 state 变量,从而排队重新渲染。

不应在渲染期间读取(或写入) current 值。

可以随时读取 state。但是,每次渲染都有自己不变的 state 快照。

useRef 内部是如何运行的?3

代码语言:javascript
复制
// 原则上 useRef 可以在 useState 的基础上 实现
function useRef(initialValue) {
  const [ref, unused] = useState({ current: initialValue });
  return ref;
}
注意:不要在渲染过程中读取或写入 ref.current

如果渲染过程中需要某些信息,请使用 state 代替。【如上述方案三,点击按钮并未渲染最新的值】

代码语言:javascript
复制
function Test1 () {
    let [counter, setCounter] = useState(0);
    return (<>
        <p>{counter}</p>
        <button onClick={() => {
            setCounter(n => n + 1);
            console.log(counter);
        }}>点我</button>
    </>)
}

function Test2 () {
    let counterRef = useRef(0);
    return (<>
        <p>{counterRef.current}</p>
        <button onClick={() => {
            counterRef.current++;
            console.log(counterRef.current);
        }}>点我</button>
    </>)
}

数据 state

显示 <p>{counter}</p>

数据 ref.current

显示 <p>{counterRef.current}</p>

第1次点击

0

1

1

0

第2次点击

1

2

2

0

第3次点击

2

3

3

0

ref 本身是一个普通的 JavaScript 对象,具有一个名为 current 的属性,可以对其进行读取或设置

由于 React 不知道 ref.current 何时发生变化,即使在渲染时读取它也会使组件的行为难以预测。

获取指向节点的 ref
代码语言:javascript
复制
export default () => {
	const inputRef = useRef(null);
	return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>
        聚焦输入框
      </button>
    </>
  );
}
获取列表节点的 ref

当 ref 数量不确定(如列表),需要为每一项都绑定 ref。

方案一:用一个 ref 引用其父元素,然后用 DOM 操作方法(如 querySelectorAll)来寻找子节点。该方案比较脆弱,当 DOM 结构发生变化,则会失效或报错。

✅方案二:将函数传递给 ref 属性,ref 回调4。当需要设置 ref 时,React 将传入 DOM 节点来调用你的 ref 回调,并在需要清除它时传入 null 。这使你可以维护自己的数组或 Map,并通过其索引或某种类型的 ID 访问任何 ref。

代码语言:javascript
复制
/* 动态添加 input 元素,并让最新添加的 input 元素获取焦点 */
const List = () => {
    const [data, setData] = useState<string[]>([])

    const inputsRef = useRef<Map<number, HTMLElement>>(null);
    function getMap(){
        if (!inputsRef.current) {
            // 首次运行时,初始化 map
            inputsRef.current = new Map();
        }
        return inputsRef.current
    }

    return (<>
        <ul style={{display: 'flex', flexDirection: 'column'}}>
            {data.map((item, index) => {
                return (
                    <li key={index}>
                        <input 
                         ref={(node) => {
                            const map = getMap();
                            // 存在追加,null删除
                            node ? map.set(index, node) : map.delete(index);
                        }}
                        type="text" 
                        value={item} 
                        onChange={console.log}/>
                    </li>
                )
            })}
        </ul>
        <button onClick={() => {
            flushSync(() => {
                setData([...data, `${Date.now()}`]);
            });
            const map = getMap();
            // 获取焦点
            map.get(data.length)?.focus();
        }}>添加</button>
    </>)
}

setData([...data, Date.now()]); 不会立即更新 DOM(state 更新是排队进行的),这里使用 flushSync(() => { ... }) 强制 React 同步更新(“刷新”)DOM。

获取自定义组件的 ref

将 ref 放在像 <input /> 这样输出浏览器元素的内置组件上时,React 会将该 ref 的 current 属性设置为相应的 DOM 节点。

默认情况下,自定义组件不会暴露它们内部 DOM 节点的 ref。

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

代码语言:javascript
复制
// forwardRef 允许组件使用 ref 将 DOM 节点暴露给父组件(父组件中按常规方式引用)
const MyInput = forwardRef((props, ref) => {
	return <input {...props} ref={ref} />;
});

延伸: 子组件内部可以使用 useImperativeHandle5 限制暴漏的功能。

代码语言:javascript
复制
const MyInput = forwardRef((props, ref) => {
  const realInputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    // 只暴露 focus,没有别的
    focus() {
      realInputRef.current.focus();
    },
  }));
  return <input {...props} ref={realInputRef} />;
});
总结

ref 是一种脱围机制,用于保留不用于渲染的值。同时,ref 是一个普通的 JavaScript 对象,具有一个名为 current 的属性,可以对其进行读取或设置。与 state 不同,设置 ref 的 current不会触发重新渲染不要在渲染过程中读取或写入 ref.current。这使组件难以预测。

  1. https://react.docschina.org/reference/react/useMemo useMemo ↩︎
  2. https://react.docschina.org/learn/escape-hatches 脱围机制 ↩︎
  3. https://react.docschina.org/learn/referencing-values-with-refs#how-does-use-ref-work-inside useRef 内部是如何运行的? ↩︎
  4. https://react.docschina.org/reference/react-dom/components/common#ref-callback ref 回调函数 ↩︎
  5. https://react.docschina.org/reference/react/useImperativeHandle useImperativeHandle ↩︎
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-05-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 方式一:子组件使用 state
  • 方式二:子组件使用 memo 包裹
  • ⚓ 方式三:父组件使用 ref
  • ref 与 state 不同之处
    • 注意:不要在渲染过程中读取或写入 ref.current
      • 获取指向节点的 ref
        • 获取列表节点的 ref
          • 获取自定义组件的 ref
          • 总结
          相关产品与服务
          云服务器
          云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档