专栏名称: 前端从进阶到入院
我是 ssh,只想用最简单的方式把原理讲明白。wx:sshsunlight,分享前端的前沿趋势和一些有趣的事情。
目录
相关文章推荐
青年文摘  ·  商场一层的“温柔陷阱”,专坑年轻人 ·  2 天前  
洞见  ·  一个人最大的无能:抱怨家人 ·  5 天前  
深夜书屋  ·  终于等来了,绝版增订重印 ·  3 天前  
青年文摘  ·  “林黛玉倒拔垂杨柳”,好笑在哪里? ·  4 天前  
51好读  ›  专栏  ›  前端从进阶到入院

一个始乱终弃的API —— forwardRef

前端从进阶到入院  · 公众号  ·  · 2024-08-28 11:28

正文

大家好,我 ssh。

React的API大多设计的很优雅,比如经典的 this.setState

但有一个API从诞生之初就争议不断,甚至很多熟练的开发者都不知道这个API存在的意义。

在最新的React19中,官方团队甚至明确提出 —— 会弃用并移除这个API。

这可真是字面意义的 「始乱终弃」

他就是 forwardRef

他在什么背景下产生?为什么争议不断?最后为什么被弃用?本文就来聊聊 forwardRef 背后的故事。

forwardRef诞生的初衷

ref reference (引用)的缩写。既然是 引用 ,那根据React元素类型的不同自然有不同的引用效果。

比如,传递给 React Element ref ,可以获得对应原生元素实例的 引用

// domRef.current 对应 HTMLInputElement

传递给类组件的 ref ,可以获得类组件实例的 引用

// instanceRef.current 对应 SomeClassComponent实例

由于函数组件没有对应实例,所以函数组件没有什么可引用的。

「语义」 来讲,函数组件自然不能接收 ref prop

但是,虽然函数组件不能接收 ref prop ,但有时函数组件有 「转发ref」 的需要。

比如,下述代码中的函数组件 FC 有个子组件 input input 作为 React Element ,有自己的实例。

function FC({
  return <input />;
}

如果 FC 的祖先元素想获得 input 实例的引用,就得让 FC 帮忙转发一下

下述代码中, FC 经过 forwardRef 包裹,就能将子组件 input 的实例转发给 inputRef 了:

const FC = forwardRef((props, ref) => {
  return <input ref={ref}/>;
});

<FC ref={inputRef}/>

forwardRef 中的 forward 从字面意义上讲就是 「转发」 的意思。

forwardRef的各种坑

forwardRef 在使用上会带来很多不便之处,这里举几个例子。

先来看比较不起眼的坑。

如果你像下面这样写,那函数组件本身是个匿名函数,在 React Dev Tools 中看不到组件名:

// 匿名的函数组件
const FC = forwardRef((props, ref) => {
  return <input ref={ref}/>;
});

必须显式的再写一遍组件名才行:

// 具名的函数组件
const FC = forwardRef(function FC(props, ref{
  return <input ref={ref}/>;
});

上面还只是小麻烦,更麻烦的是 forwardRef TS 结合时造成的类型书写复杂。

如果你的函数组件接收范型类型,当该函数组件被 forwardRef 包裹,范型推导就会失效。

需要使用一些很 hack 的类型来断言 forwardRef

文章 TypeScript + React: Typing Generic forwardRefs 详细介绍了forwardRef + TS的各种坑

如果说以上坑都只是带来不便,那下面这个坑就是实打实的性能问题了。

由于 forwardRef 会在组件树中生成额外的层级,在应用比较庞大时可能产生性能问题。

比如, React-Redux 的维护者 markerikson 在给 React-Redux 做性能基准测试时就发现, forwardRef 会对页面 FPS 指标带来不利影响。

类似上面这些问题使 forwardRef 在社区的争议一直很大。

官方的态度

官方很早就意识到 forwardRef 的局限性,早在2019年, sebmarkbage 就在 RFC #107 [1] 指出 「未来,最终会移除forwardRef」

Andrew 在23年也表达过 「forwardRef未来一定会被移除」 ,之所以没有立刻做,是因为 「移除一个被大量使用的API属于Breaking Change」

按照 Semantic Versioning (即形如 A.B.C 的版本号格式)的定义, 「不向下兼容的变化」 需要在主版本(即 A )中体现。

所以 「移除forwardRef」 就和其他Breaking Change(比如移除 propTypes defaultProps )一起被包含在v19中。

促使官方最终决定移除 forwardRef 的,除了来自社区的各种抱怨,还有个重要的原因 —— ref 最初的语义(对实例的引用)是在类组件盛行的时代产生的。随着 Hooks 的崛起,函数组件的重要性远超类组件。

既然类组件终将消失在历史中,那严守 ref 的语义就没意义了。

「不再执着于ref语义」 也带来了另一个源码层面的变化。

如果你细心观察会发现,对于 React Element 与类组件,传递进来的 ref 会被 「自动赋予对实例的引用」 ,而传递给 forwardRef ref ,需要开发者手动决定怎么用:

// domRef.current 自动获得 HTMLInputElement


// instanceRef.current 自动获得 SomeClassComponent实例
<SomeClassComponent ref={instanceRef} />

forwardRef((props, ref) => {
  // 开发者自己决定将 ref.current 赋值给谁
  // ...
});

这种区别导致在类组件中,如果用解构对象的方式操作 props ,很可能意外将 ref 传递下去并自动获得实例引用。

比如下面代码中,如果 otherProps 中包含 ref ,就会被传递给 SomeClassComponent 并获得 SomeClassComponent 实例的引用:

let {myCustomProp, ...otherProps} = props;
doSomething(myCustomProp);
return <SomeClassComponent {...otherProps} />;

所以, ref key 作为 JSX 中两个特殊字段会被特殊处理,而其他 props 会被直接透传。

鉴于:

  1. 未来 forwardRef 被废弃

  2. 类组件被冷落

  3. 函数组件不会自动决定 ref 的赋值

未来 ref 也将变成一个 「可以被透传的普通prop」

总结

本文详细解释了 forwardRef

  1. 产生的背景、作用

  2. 存在的问题

  3. React核心团队对此的思考

往深了说, forwardRef 从出现到消失,反映的其实是一个权力问题 —— 类组件失势,导致失去对 ref 语义的定义权。







请到「今天看啥」查看全文