1、什么是Refs?
官网是这么说的:
Refs 提供了一种形式,容许咱们拜访 DOM 节点或在 render 办法中创立的 React 元素。
在react数据流中,props是父子组件交换的惟一形式。如果要批改一个子组件的状态,那么就须要应用新的props去从新渲染。然而,在某些状况下,咱们须要在props数据流之外,强制去批改子组件。被批改的子组件可能是一个React组件的实例,也可能是DOM元素。React官网对于这两种状况都给出了解决办法。
2、类组件中应用ref
React.createRef()
这种形式是应用 React.createRef() 创立的ref,并通过 ref 属性附加到 React 元素。在结构组件时,通常将 Refs 调配给实例属性,以便能够在整个组件中援用它们。
当 ref 被传递给 render 中的元素时,对该节点的援用能够在 ref 的 current 属性中被拜访。
ref 的值依据节点的类型而有所不同:
- 当 ref 属性用于 HTML 元素时,构造函数中应用 React.createRef() 创立的 ref 接管底层 DOM 元素作为其 current 属性。
- 当 ref 属性用于自定义 class 组件时,ref 对象接管组件的挂载实例作为其 current 属性。
- 你不能在函数组件上应用 ref 属性,因为他们没有实例。
export class App extends React.Component { constructor(props) { super(props); this.myDOM = React.createRef(); this.myComponent = React.createRef(); this.myFunction = React.createRef(); } log() { console.log('myDOM', this.myDOM); console.log('myComponent', this.myComponent); console.log('myFunction', this.myFunction); } render() { return ( <div> <button onClick={() => this.log()}> click</button> <div ref={this.myDOM}>DOM</div> <MyComponent ref={this.myComponent} /> <MyFunction ref={this.myFunction} /> </div> ) }}class MyComponent extends React.Component { constructor(props) { super(props); } render() { return <div>MyComponent</div>; }}function MyFunction(params) { return ( <div>MyFunction</div> )}
callback Ref
export class App extends React.Component { constructor(props) { super(props); this.myDOM = null this.myComponent = null this.myFunction = null } log() { console.log('myDOM', this.myDOM); console.log('myComponent', this.myComponent); console.log('myFunction', this.myFunction); } render() { return ( <div> <button onClick={() => this.log()}> click</button> <div ref={ele => this.myDOM = ele}>DOM</div> <MyComponent ref={ele => this.myComponent = ele} /> <MyFunction ref={ele => this.myFunction = ele} /> </div> ) }}class MyComponent extends React.Component { constructor(props) { super(props); } render() { return <div>MyComponent</div>; }}function MyFunction(params) { return ( <div>MyFunction</div> )}
React 将在组件挂载时,会调用 ref 回调函数并传入 DOM 元素,当卸载时调用它并传入 null。在 componentDidMount 或 componentDidUpdate 触发前,React 会保障 refs 肯定是最新的。
如果要在函数组件中应用 ref,你能够应用 forwardRef(可与 useImperativeHandle 联合应用),或者能够将该组件转化为 class 组件。
不管怎样,你能够在函数组件外部应用 ref 属性,只有它指向一个 DOM 元素或 class 组件。
3、forwardRef
Ref 转发是一项将 ref 主动地通过组件传递到其一子组件的技巧
通过forwardRef能够获取函数组件中的DOM元素
export class App extends React.Component { constructor(props) { super(props); this.myDOM = null this.myComponent = null this.myFunction = null } log() { console.log('myDOM', this.myDOM); console.log('myComponent', this.myComponent); console.log('myFunction', this.myFunction); } render() { return ( <div> <button onClick={() => this.log()}> click</button> <div ref={ele => this.myDOM = ele}>DOM</div> <MyComponent ref={ele => this.myComponent = ele} /> <MyFunction ref={ele => this.myFunction = ele} /> </div> ) }}class MyComponent extends React.Component { constructor(props) { super(props); } render() { return <div>MyComponent</div>; }}const MyFunction = React.forwardRef((props, ref) => { return ( <div ref={ref}>MyFunction</div> )})
4、useRef的应用
const refContainer = useRef(initialValue);
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内放弃不变。
export function App() { return <TextInputWithFocusButton />}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不仅能够保留DOM元素和class组件实例,也能够用来保留任何值。
export function App() { return <TextInputWithFocusButton />}function TextInputWithFocusButton() { const inputEl = useRef(90); const onButtonClick = () => { console.log(inputEl); }; return ( <> <button onClick={() => inputEl.current += 1}>+</button> <button onClick={() => inputEl.current -= 1}>-</button> <button onClick={onButtonClick}>Focus the input</button> <div>{inputEl.current}</div> </> );}
inputEl的current属性值为90,点击"+" 按钮将会在inputEl.current的根底上+1,扭转valRef.current不会触发组件的更新
因而咱们须要别的形式来在ref.current变更时,触发组件更新,展现最新的ref.current值,应用useState强制更新。
export function App() { return <TextInputWithFocusButton />}function TextInputWithFocusButton() { const inputEl = useRef(90); const [state, setState] = useState({}) const onButtonClick = () => { console.log(inputEl); }; return ( <> <button onClick={() => { inputEl.current += 1; setState({}) }}>+</button> <button onClick={() => { inputEl.current -= 1; setState({}) }}>-</button> <button onClick={onButtonClick}>Focus the input</button> <div>{inputEl.current}</div> </> );}
React.createRef能够用于class组件和Function组件,React.useRef必须用于函数组件。当React.createRef()用于Fucntion组件的时候,可能呈现一些问题。
export function App() { return <TextInputWithFocusButton />}function TextInputWithFocusButton() { const inputEl = React.createRef(); const [state, setState] = useState({}); useEffect(() => { inputEl.current = 100; }, []) const onButtonClick = () => { console.log(inputEl); }; return ( <> <button onClick={() => { inputEl.current += 1; setState({}) }}>+</button> <button onClick={() => { inputEl.current -= 1; setState({}) }}>-</button> <button onClick={onButtonClick}>Focus the input</button> <div>{inputEl.current}</div> </> );}
咱们用createRef替换掉useRef,在这里无论点击多少次按钮,最初还是会显示为空(值为null):
这是因为函数组件的行为依然是函数,他们在渲染和重渲染的 就像一般的函数一样
所以,当App这个函数组件被从新渲染时,App函数将会执行,并且从新创立、初始化所有的变量和表达式。因而,createRef每次都会被执行,所以对应的值总是为null。
5、useRef解决了什么问题
为了在函数组件中保留状态,useRef就被发明进去,它将会在函数组件的生命周期中,放弃状态不变,除非手动进行批改。
函数式组件中,咱们应该应用useRef去创立咱们的ref对象,useRef 总会在每次渲染时返回同一个 ref 对象,除非手动扭转。
然而,当 ref 对象内容发生变化时,useRef 并不会告诉你。变更 .current 属性不会引发组件从新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则须要应用回调 ref 来实现。
export function App() { return <MeasureExample />}function MeasureExample() { const [height, setHeight] = useState(0); const measureRef = useCallback(node => { if (node !== null) { setHeight(node.getBoundingClientRect().height); } }, []); return ( <> <Child measureRef={measureRef} /> {height > 0 && <h2>The above header is {Math.round(height)}px tall</h2> } </> );}function Child({ measureRef }) { const [show, setShow] = useState(false); if (!show) { return ( <button onClick={() => setShow(true)}> Show child </button> ); } return ( <> <button onClick={() => setShow(false)}> Show child </button> <h1 ref={measureRef}>Hello, world</h1> </> )}
在这里,咱们没有抉择应用useRef,而是应用了Callback Ref能够确保 即使子组件提早显示被测量的节点(比方为了响应一次点击),咱们仍然可能在父组件接管到相干的信息,以便更新测量后果。
在此示例中,当且仅当组件挂载和卸载时,callback ref 才会被调用,因为渲染的 <h1> 组件在整个从新渲染期间始终存在。如果你心愿在每次组件调整大小时都收到告诉,则可能须要应用 ResizeObserver 或基于其构建的第三方 Hook。
6、useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle能够让你在应用ref时自定义裸露给父组件的实例。不要间接应用forward Ref,forward Ref应该与useImperativeHandle一起应用
- ref:定义 current 对象的 ref createHandle:一个函数,返回值是一个对象,即这个 ref 的 current
- 对象 [deps]:即依赖列表,当监听的依赖发生变化,useImperativeHandle 才会从新将子组件的实例属性输入到父组件
- ref 的 current 属性上,如果为空数组,则不会从新输入。
它解决了什么问题
下面咱们讲述了forward Ref的一些问题,forward Ref会将子组件的实例间接裸露给父组件,父组件能够对子组件实例做任意的操作,那么,useImperativeHandle就是来解决这个问题的,useImperativeHandle能够让咱们抉择裸露一些咱们须要被非凡解决的操作。
- 通过useImperativeHandle的Hook, 将父组件传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起
- 所以在父组件中, 调用inputRef.current时, 实际上是返回的对象
- 缩小裸露给父组件获取的DOM元素属性, 只裸露给父组件须要用到的DOM办法
间接应用forward Ref
export function App() { return <Example />}// 实现 ref 的转发const FancyButton = React.forwardRef((props, ref) => ( <div> <input ref={ref} type="text" /> <button>{props.children}</button> </div>));// 父组件中应用子组件的 reffunction Example() { const ref = useRef(); const handleClick = useCallback(() => ref.current.focus(), [ ref ]); return ( <div> <FancyButton ref={ref}>Click Me</FancyButton> <button onClick={handleClick}>获取焦点</button> </div> )}
配合useImperativeHandle一起应用
export function App() { return <Example />}const FancyInput = React.forwardRef((props, ref) => { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return <input ref={inputRef} type="text" />});const Example = () => { const fancyInputRef = useRef(); return ( <div> <FancyInput ref={fancyInputRef} /> <button onClick={() => fancyInputRef.current.focus()} >父组件调用子组件的 focus</button> </div> )}
下面这个例子中与间接转发 ref 不同,间接转发 ref 是将 React.forwardRef 中函数上的 ref 参数间接利用在了返回元素的 ref 属性上,其实父、子组件援用的是同一个 ref 的 current 对象,官网不倡议应用这样的 ref 透传,而应用 useImperativeHandle 后,能够让父、子组件别离有本人的 ref,通过 React.forwardRef 将父组件的 ref 透传过来,通过 useImperativeHandle 办法来自定义凋谢给父组件的 current。
useImperativeHandle 的第一个参数是定义 current 对象的 ref,第二个参数是一个函数,返回值是一个对象,即这个 ref 的 current 对象,这样能够像下面的案例一样,通过自定义父组件的 ref 来应用子组件 ref 的某些办法
export function App() { return <Example />}const FancyInput = React.forwardRef((props, ref) => { const [fresh, setFresh] = useState(false) const attRef = useRef(0); useImperativeHandle(ref, () => ({ attRef, fresh }), [fresh]); const handleClick = useCallback(() => { attRef.current++; }, []); return ( <div> {attRef.current} <button onClick={handleClick}>Fancy</button> <button onClick={() => setFresh(!fresh)}>刷新</button> </div> )});const Example = () => { const fancyInputRef = useRef(); return ( <div> <FancyInput ref={fancyInputRef} /> <button onClick={() => console.log(fancyInputRef.current)} >父组件拜访子组件的实例属性</button> </div> )}