Refs
一个专业术语
提供了一个可访问 DOM
或 React 元素
的方式
简单点说,提供了可手动获取并直接
操作 DOM
的方式
比如 input
元素聚焦
文件上传功能等
而干活的是 ref
属性
名词解释
- ClassComponent 类组件
- FunctionComponent 函数组件
- React Hook 钩子
- callback ref 回调 ref
属性值类型根据
ref
绑定的节点类型而不同
- ref 绑定节点类型为 DOM 元素 ref 表示这个具体 DOM 元素
- ref 绑定节点类型为 ClassComponent ref 表示这个类组件(实例)
- ref 绑定节点类型为 FunctionComponent,注意不能直接绑定
- 因为它不是 DOM 或 ClassComponent
- 本身
props
上默认不存在ref
,通过React.forwardRef
函数转发时存在,其内部要绑定 DOM 或 ClassComponent 才可以(本质绑定 DOM 或 ClassComponent)
创建 ref 方式
除 callback ref
外
无论哪种方式创建 ref
引用
都包含 current
属性,包含 ref 具体值
- React.createRef 方式
- 用于 ClassComponent,不建议用于 FunctionComponent,在
FunctionComponent
重新渲染被初始化创建新的 ref 引用(详情参考下面的差异对比) - 不可以传参
current
属性在Typescript
中提示为只读
createRef<T>
泛型函数无默认值,被设置为unknow
- 用于 ClassComponent,不建议用于 FunctionComponent,在
- React.useRef 方式
React Hook
一种,遵循 Hook 规则,用于 FunctionComponent 或其他 Hook, 处于函数作用域顶层被调用- 可用于数据存储或性能优化,数据保存到
current
属性上 - 可传参,参数类型任意
- 创建一次,更新渲染不创建只复用
useRef<T = undefined>
泛型函数默认值为undefined
current
属性值变化不可被监测到,同时不触发组件render
视图更新
- callback ref 方式
- 解决 ref 值变化无法被监测
- ClassComponent 内联绑定或 FunctionComponent 绑定时,再次更新时被调用两次,第一次打印值为
null
,第二次为具体绑定的node
对象,原因每次渲染时会创建一个新的函数组件实例(此时 callback ref 也重新初始化),React 清空旧的 ref 并且设置新的 - 在 ClassComponent 中,
callback ref
绑定内联函数上时,初始化render
调用一次,再次render
调用两次
export default class Child extends React.PureComponent { state = { a: 100 } constructor(props: any) { super(props) } onCount() { this.setState({ a: Math.random() }) } render() { return ( <div> <p ref={(node: any)=> { // 初始化打印一次 结果:node 对象 // 下次渲染更新,打印两次 结果:第一次为 null, 第二次为 node 对象 console.log(node) }}>{this.state.a}</p> <button className="btn btn-primary" onClick={this.onCount.bind(this)}>click</button> </div> ) } }
callback ref
绑定实例方法,初始化render
调用一次,再次render
不调用
export default class Child extends React.PureComponent { state = { a: 100 } cbRef: (node: any) => void constructor(props: any) { super(props) this.cbRef = (node: any)=> { // 初始化与再渲染更新,仅打印一次 // 结果:node 对象 console.log(node) } } onCount() { this.setState({ a: Math.random() }) } render() { return ( <div> <p ref={this.cbRef}>{this.state.a}</p> <button className="btn btn-primary" onClick={this.onCount.bind(this)}>click</button> </div> ) } }
- 在 FunctionComponent 中,
callback ref
无论是否内联绑函数,初始化打印一次,再次渲染更新,打印两次function User(_props: any) { let [data] = useState({ foo: 1 }) const [count, setCount] = useState(0) const renderRef = (node: any)=> { // 初始化打印一次 结果:node 对象 // 下次渲染更新,打印两次 结果:第一次为 null, 第二次为 node 对象 console.log(node) } return ( <> <h3>{data.foo}</h3> <p ref={renderRef}>count: {count}</p> <button className="btn btn-primary" onClick={() => setCount(count + 1)}>click me</button> </> ) } export default User
ref 转发之 forwardRef
FunctionComponent
无 ref
属性
父组件无法直接通过 ref
引用 FunctionComponent 组件获取 DOM 元素
利用 forwardRef 转发特性
转发后的 FunctionComponent
组件具有 ref
属性
将父组件 ref 绑定到 DOM 元素上
在父组件中可获取到子组件的具体 DOM 元素进行操作
function InputChild(props: any) {
return (
<input type="text" ref={props.inputRef} className="form-control" placeholder="说点什么" />
)
}
const ForwardInput = forwardRef((props, ref: any) => <InputChild inputRef={ref} />)
function User(_props: any) {
const ref = useRef<HTMLInputElement>()
return (
<>
{/* 父组件通过 InputChild 转发返回后的组件设置 ref 属性 */}
{/* 获取 InputChild 组件中 input 元素 */}
{/* 父组件点击按钮设置 input 元素焦点 */}
<ForwardInput ref={ref} />
<button className="btn btn-primary" onClick={() => ref?.current?.focus()}>input focus</button>
</>
)
}
export default User
FunctionComponent 中
createRef
与useRef
差异对比
如上图:
createRef
每次渲染新建,所以始终为 null
useRef
只创建一次,再渲染时复用,所以引用同一个 p
元素
FunctionComponent
推荐使用 useRef
源码如下:
function User(_props: any) {
const ref = useRef(null)
const ref2 = createRef<HTMLParagraphElement>()
const [count, setCount] = useState(0)
useEffect(() => {
}, [count])
console.log(count)
console.log('useRef:', ref, 'createRef:', ref2)
return (
<>
<h3>{count}</h3>
<p ref={ref}>useRef p</p>
<p ref={ref2}>createRef p</p>
<button className="btn btn-primary" onClick={() => setCount(count + 1)}>click count</button>
</>
)
}
export default User