前端进阶系列——理解 React Ref

发布时间 2023-10-11 11:43:05作者: 漫思

前端进阶系列——理解 React Ref

 
Ref 是 Reference(引用) 的缩写。

一、前言

在 React 中通常遵循 “自上而下” 的 “单向数据流”。父组件和子组件的通讯只能通过 Props。如果要修改一个子组件,我们要修改 Props,让 React 重新渲染子组件。

但是有时候,我们需要用数据流之外的方式来修改子组件,例如:获取焦点、视频开始播放等。

Ref 提供了这个方式,让我们可以直接操作子元素。

二、什么是Ref?—— “命令式” 操作组件

Props 是单向数据流,以 “声明式” 渲染组件;Ref 则是以 “命令式” 操作组件。

下面举例来体会声明式命令式的区别,实现下图功能:

 

2.1 以声明式的方式:

  1. 声明一个 focused 的 state
  2. 作为 Props 传给子组件 <input focused={focused} />
  3. 点击按钮时:修改 focused 为 true
function App() { 
  const [focused, setFocused] = useState(false); 
  return ( 
    <div> 
      <button onClick={() => setFocused(true)}>开始输入</button> 
      <input focused={focused} placeholder="我是输入框" /> 
    </div> 
  ); 
} 

但是 input 组件并没有 focused 参数。因此我们需要操作dom,命令式调用dom.focus()来获取焦点。

2.2 使用 Ref,以命令式的方式:

  1. 声明一个 inputRef,用于接受 inputDom
  2. 把 inputRef 传递给 input 进行设置,<input ref={inputRef} />
  3. 点击按钮时:操作dom,主动调用 focus()
function App() { 
  const inputRef = React.useRef(); 
 
  function handleClick() { 
    // 按钮点击时,命令式的调用dom.focus方法 
    inputRef.current && inputRef.current.focus(); 
  }
 
  return ( 
    <div className="App"> 
      <button onClick={handleClick}>开始输入</button> 
      <input ref={inputRef} placeholder="我是输入框" /> 
    </div> 
  ); 
} 

这就是命令式,打破了 Props 的单向数据流,直接操作子元素。

2.3 Ref 使用场景

重要提示:因为命令式破坏了原先的数据流,所以请不要滥用 Ref。

可以使用 Props 完成的,建议优先使用声明式的Props。例如:我们写一个“对话框组件“,最好使用 isOpen 属性控制开关,而不是暴露 close() 和 open() 方法。

总的来说,Ref 通常有三类场景:

  • 处理 focus、视频播放 等
  • 操作 dom 进行的动画
  • 集成第三方的 dom 库

三、Ref 各类使用姿势

3.1 回调式的 Ref

Ref 还可以传入一个函数,开发者可以在这个函数里面保存 dom 的引用,更自由地设置引用。

回调 Ref实现上一节的例子:

function App() { 
  let inputElement = null; 
 
  /** Ref的回调函数,保存node的引用 */ 
  function setElement (node) { 
    inputElement = node; 
  } 
 
  function handleClick() { 
    // 直接使用引用 
    inputElement && inputElement.focus(); 
  } 
 
  return ( 
    <div className="App"> 
      <button onClick={handleClick}>开始输入</button> 
      {/* 传入回调函数 */} 
      <input ref={setElement} /> 
    </div> 
  ); 
} 

Tips:上面的例子,当组件发生更新时:

  • setElement 会执行两次。第一次参数传入 null:setElement(null),清空旧的引用。第二次传入 dom 元素:setElement(newDom)。
  • 因为对于函数组件而言,在每次渲染时会创建一个新的函数实例。所以第一次清空旧的 Ref,第二次在新实例下的设置引用。两次调用其实是针对不同的 inputElement 对象。
  • 旧的函数实例,调用一次 setElement(null) 正好可以帮我们释放一些引用,防止泄露。

3.2 Ref 转发

“Ref 转发” 就是让组件接收 Ref,然后向下传递给子组件。一般场景不常用,在写一些通用组件的时候,会用到。

3.2.1 React.forwardRef 使用

React 提供了 forwardRef,让我们可以做到转发。

例如我们要封装一个公共的 Button 组件。节选自 Antd,伪代码:

const InternalButton = (props, ref) => { 
    return <button 
      className="common-button" 
      ref={ref} 
    > 
        ... 
    </button> 
} 
 
const Button = React.forwardRef(InternalButton); 
export default Button; 

使用时:

const ref = React.useRef(); 
<Button ref={ref}>Click me!</Button>; 

此时,获取到的 ref 就是组件内部真实的 button。

PS:组件的第二个参数 ref 只在forwardRef 定义组件时存在。常规 props 里没有此参数。

3.2.2 useImperativeHandle 使用

当然,除了 dom 元素之外,Ref 还可以指向其他对象。

Ref 是命令式的编程,有时对于一些复杂的场景,我们希望自定义 Ref 里的命令。

此时useImperativeHandle登场。举例:

const FancyButton = forwardRef((props, ref) => { 
  const internalRef = useRef(); 
  useImperativeHandle(ref, () => ({ 
    click: () => { 
      // ...更多你想要的处理逻辑 
      internalRef.current.click(); 
    } 
  })); 
  return <button ref={internalRef} ... />; 
}); 

在本例中,渲染 <FancyButton ref={buttonRef} /> 的父组件可以调用 buttonRef.current.click()

3.3 Ref 的一些魔法

我们可以用 Ref 完成很多奇思妙想的 “魔法”。

3.3.1 魔法1:记录先前的状态 —— 用 Ref 实现 usePrevious

以前用过类组件的同学,切换到函数组件。总会有疑问:previousValue 怎么实现?

function Counter() { 
  const [count, setCount] = useState(0);
  const prevCountRef = useRef(); 
  useEffect(() => { 
    prevCountRef.current = count; 
  }, [count]);
  return ( 
    <h1> 
      Now: {count}, before: {prevCountRef.current} 
      <button onClick={() => setCount((count) => count + 1)}>Increment</button> 
    </h1> 
  ); 
}
保存上次的值

当然,我们可以用这个“魔法”,封装一个 hook —— usePrevious

const usePrevious = value => { 
    const ref = useRef(); 
    useEffect(()=> { 
        ref.current = value; 
    }); 
    return ref.current; 
} 

使用它:

const [count, setCount] = useState(0); 
const prevCount = usePrevious(count); 

3.3.2 魔法2:动态获取 dom 的宽高

可以用 Ref 获取 dom 引用,获取 offsetWidthoffsetHeight

function App() { 
  const ref = useRef(null); 
 
  useEffect(() => { 
    console.log("width", ref.current.offsetWidth); 
  }, []); 
 
  return <div ref={ref}>Hello</div>; 
}

四、总结

综上所述:

  • 声明式:React 推荐的单向数据流,使用 Props
  • 命令式:React Ref(引用)

请尽量使用声明式来完成我们的组件,当 Props 做不到时,我们再使用 Ref。不要滥用!不要滥用!

五、相关资料

参考资料 :

相关阅读 :