什么是refs
Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
在组件mount
之后再去获取ref
。componentWillMount
和第一次render
时都获取不到,在componentDidMount
才能获取到
refs的使用方式
类组件
字符串(string ref) React v16.3 之前
1
2
3
4
5
6
7
8
9// string ref
class MyComponent extends React.Component {
componentDidMount() {
this.refs.myRef.focus();
}
render() {
return <input ref="myRef" />;
}
}回调函数(callback ref)React v16.3 之前
1
2
3
4
5
6
7
8
9
10
11// callback ref
class MyComponent extends React.Component {
componentDidMount() {
this.myRef.focus();
}
render() {
return <input ref={(ele) => {
this.myRef = ele;
}} />;
}
}React.createRef React v16.3提出 简单有效的方案
1
2
3
4
5
6
7
8
9
10
11
12
13// React.createRef
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
componentDidMount() {
this.myRef.current.focus();
}
render() {
return <input ref={this.myRef} />;
}
}
函数组件
useRef
1
const refContainer = useRef(initialValue);
useRef返回一个可变的ref对象,其
.current
属性被初始化为传入的参数(initialValue)
。返回的ref对象在整个生命周期内保持不变。
例子:
1 | function TextInputWithFocusButton() { |
先通过useRef
创建一个ref对象inputEl
,点击button,然后再将inputEl
赋值给input
的ref
,最后,通过inputEl.current.focus()
就可以让input聚焦。
假如 input不是一个dom元素,而是一个子组件,就需要用到forwardRef。
forwardRef
将input单独封装成一个组件TextInput
1 | const TextInput = forwardRef((props,ref) => { |
然后用TextInputWithFocusButton
调用它
1 | function TextInputWithFocusButton() { |
可以看到React.forwardRef 接受一个渲染函数,其接收 props 和 ref 参数并返回一个 React 节点。
这样我们就将父组件中创建的ref
转发进子组件,并赋值给子组件的input元素,进而可以调用它的focus方法。
至此为止,通过useRef+forwardRef,我们就可以在函数式组件中使用ref了。
有时候,我们可能不想将整个子组件暴露给父组件,而只是暴露出父组件需要的值或者方法,这样可以让代码更加明确。而useImperativeHandle
Api就是帮助我们做这件事的。
useImperativeHandle
1 | useImperativeHandle(ref, createHandle, [deps]) |
useImperativeHandle
可以让你在使用 ref 时自定义暴露给父组件的实例值。
例子:
1 | /*子组件*/ |
这样,我们也可以使用current.focus()来事input聚焦。这里要注意的是,子组件TextInput中的useRef对象,只是用来获取input元素的,大家不要和父组件的useRef混淆了。
回调Ref
当 ref
对象内容发生变化时,useRef
并不会通知你。变更 .current
属性不会引发组件重新渲染,通俗点就是子组件的数据更新不会实时传到子组件,看下面这个例子:
1 | /*父组件*/ |
父组件获取不到子组件实时的值,必须点击按钮才能获取到,即使我写了useEffect
,希望它在inputEl
改变的时候,重新设置value
的值。
修改后的代码:
1 | /*父组件*/ |
输入时,父组件就可以实时地拿到子组件输入的值了。
这里比较关键的代码就是使用useCallback
代替了useRef
,callback ref
会将当前ref的值变化通知到父组件。
场景
- 对input/video/audio需要控制时,例如输入框焦点、媒体播放状态
- 触发强制动画
- 集成第三方库(传递 dom 节点进去)
注意:如果能使用props实现,应该尽量避免使用refs实现
ref拿到的到底是什么
很多文章里面说ref拿到的是真实DOM节点。其实这种说法很笼统。也很让人困惑,上面看到拿到的要么是实例(我们自定义组件)要么是component的_hostNode属性,这个好像不是真实DOM
- 什么是_hostNode?
是通过document.createElement方法创建的element对象。只是这时候对象保存在了虚拟DOM中,然后再塞入真实DOM树。所以说_hostNode和真实DOM树中的DOM的关系就是不同对象的不同属性指向的同一块存储空间,引用着同一个值而已。
虽然是通过虚拟DOM的_hostNode拿到这个值,但是对他的操作会体现在真实DOM节点上。说白了就是对象的引用赋值。
所以,ref拿到的是真实DOM的引用这个说法更准确。