什么是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
| 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
| 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
| class MyComponent extends React.Component { constructor(props) { super(props); this.myRef = React.createRef(); } componentDidMount() { this.myRef.current.focus(); } render() { return <input ref={this.myRef} />; } }
|
函数组件
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13
| function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); }
|
先通过useRef
创建一个ref对象inputEl
,点击button,然后再将inputEl
赋值给input
的ref
,最后,通过inputEl.current.focus()
就可以让input聚焦。
假如 input不是一个dom元素,而是一个子组件,就需要用到forwardRef。
forwardRef
将input单独封装成一个组件TextInput
1 2 3
| const TextInput = forwardRef((props,ref) => { return <input ref={ref}></input> })
|
然后用TextInputWithFocusButton
调用它
1 2 3 4 5 6 7 8 9 10 11 12 13
| function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { inputEl.current.focus(); }; return ( <> <TextInput ref={inputEl}></TextInput> <button onClick={onButtonClick}>Focus the input</button> </> ); }
|
可以看到React.forwardRef 接受一个渲染函数,其接收 props 和 ref 参数并返回一个 React 节点。
这样我们就将父组件中创建的ref
转发进子组件,并赋值给子组件的input元素,进而可以调用它的focus方法。
至此为止,通过useRef+forwardRef,我们就可以在函数式组件中使用ref了。
有时候,我们可能不想将整个子组件暴露给父组件,而只是暴露出父组件需要的值或者方法,这样可以让代码更加明确。而useImperativeHandle
Api就是帮助我们做这件事的。
useImperativeHandle
1
| useImperativeHandle(ref, createHandle, [deps])
|
useImperativeHandle
可以让你在使用 ref 时自定义暴露给父组件的实例值。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const TextInput = forwardRef((props,ref) => { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return <input ref={inputRef} /> })
function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { inputEl.current.focus(); }; return ( <> <TextInput ref={inputEl}></TextInput> <button onClick={onButtonClick}>Focus the input</button> </> ); }
|
这样,我们也可以使用current.focus()来事input聚焦。这里要注意的是,子组件TextInput中的useRef对象,只是用来获取input元素的,大家不要和父组件的useRef混淆了。
回调Ref
当 ref
对象内容发生变化时,useRef
并不会通知你。变更 .current
属性不会引发组件重新渲染,通俗点就是子组件的数据更新不会实时传到子组件,看下面这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| function TextInputWithFocusButton() { const inputEl = useRef(null); const [value, setValue] = useState(""); useEffect(() => { setValue(inputEl.current.value); }, [inputEl]); const onButtonClick = () => { console.log("input值", inputEl.current.value); setValue(inputEl.current.value); }; return ( <> <div> 子组件: <TextInput ref={inputEl}></TextInput> </div> <div> 父组件: <input type="text" value={value} onChange={() => {}} /> </div> <button onClick={onButtonClick}>获得值</button> </> ); }
const TextInput = forwardRef((props, ref) => { const [value, setValue] = useState(""); const inputRef = useRef(); useImperativeHandle(ref, () => ({ value: inputRef.current.value, })); const changeValue = e => { setValue(e.target.value); }; return <input ref={inputRef} value={value} onChange={changeValue}></input>; });
|
父组件获取不到子组件实时的值,必须点击按钮才能获取到,即使我写了useEffect
,希望它在inputEl
改变的时候,重新设置value
的值。
修改后的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| function TextInputWithFocusButton() { const [value, setValue] = useState(""); const inputEl = useCallback(node => { if (node !== null) { console.log("TCL: TextInputWithFocusButton -> node.value", node.value) setValue(node.value); } }, []); return ( <> <div> 子组件: <TextInput ref={inputEl}></TextInput> </div> <div> 父组件: <input type="text" value={value} onChange={() => {}} /> </div> </> ); }
const TextInput = forwardRef((props,ref) => { const [value, setValue] = useState('') const inputRef = useRef(); useImperativeHandle(ref, () => ({ value: inputRef.current.value })); const changeValue = (e) =>{ setValue(e.target.value); } return <input ref={inputRef} value={value} onChange={changeValue}></input> })
|
输入时,父组件就可以实时地拿到子组件输入的值了。
这里比较关键的代码就是使用useCallback
代替了useRef
,callback ref
会将当前ref的值变化通知到父组件。
场景
- 对input/video/audio需要控制时,例如输入框焦点、媒体播放状态
- 触发强制动画
- 集成第三方库(传递 dom 节点进去)
注意:如果能使用props实现,应该尽量避免使用refs实现
ref拿到的到底是什么
很多文章里面说ref拿到的是真实DOM节点。其实这种说法很笼统。也很让人困惑,上面看到拿到的要么是实例(我们自定义组件)要么是component的_hostNode属性,这个好像不是真实DOM
是通过document.createElement方法创建的element对象。只是这时候对象保存在了虚拟DOM中,然后再塞入真实DOM树。所以说_hostNode和真实DOM树中的DOM的关系就是不同对象的不同属性指向的同一块存储空间,引用着同一个值而已。
虽然是通过虚拟DOM的_hostNode拿到这个值,但是对他的操作会体现在真实DOM节点上。说白了就是对象的引用赋值。
所以,ref拿到的是真实DOM的引用这个说法更准确。
参考:源码看React—- ref