🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] `React`中的`Refs`提供了一种访问`render()`方法中创建的`React`元素(或`DOM`节点)的方法。 当父组件需要与子组件交互时,我们通常使用 [props](https://reactjs.org/docs/components-and-props.html) 来传递相关信息。 但是,**在某些情况下,我们可能需要修改子项,而不用新的`props`重新呈现 (re-rendering) 它**。这时候就需要`refs`出场了。 # 什么时候使用 Refs ? 我们建议在以下情况下使用 `refs`: * 与第三方 `DOM` 库集成 * 触发命令式动画 * 管理焦点,文本选择或媒体播放 > 译注:第三点是否也可以理解为使用 `event` 对象呢?在 React 中就是合成事件 (SyntheticEvent)。 > **官方文档中提到:避免使用 `refs` 来做任何可以通过声明式实现来完成的事情**。 所以一旦我们确定我们需要使用 `refs`,我们如何使用它们呢? # 在 React 中使用 Refs 您可以通过多种方式使用`refs`: * [React.createRef()](https://reactjs.org/docs/refs-and-the-dom.html) * 回调引用 (Callback refs) * String refs(已过时,这 API 将被弃用) * 转发`refs`(Forwarding refs) ## `React.createRef()` 使用`React.createRef()`创建引用,并通过`ref`属性附加到React元素上。 在构造组件时,通常将 Refs 分配给实例属性,以便在整个组件中引用它们。 ``` // Ref.js class CustomTextInput extends React.Component { constructor(props) { super(props); // create a ref to store the textInput DOM element this.textInput = React.createRef(); // 先在 构造函数中创建并挂载在组件的一个属性上,然后就可以在该组件上使用了 this.focusTextInput = this.focusTextInput.bind(this); } focusTextInput() { // Explicitly focus the text input using the raw DOM API // Note: we're accessing "current" to get the DOM node this.textInput.current.focus(); } render() { // tell React that we want to associate the <input> ref // with the `textInput` that we created in the constructor return ( <div> <input type="text" ref={this.textInput} /> <input type="button" value="Focus the text input" onClick={this.focusTextInput} /> </div> ); } } ``` 在上面的代码块中,我们构建了一个按钮,当单击它时,**该页面会自动聚焦在输入框上。** 首先,我们在构造方法中**创建一个 React `ref` 实例**,并将其赋值给 `this.textInput`,然后通过`ref` 属性将其分配给 `input`元素。 ``` <input type="text" ref={this.textInput} /> ``` 注意,当 `ref` 属性被一个`HTML` 元素使用时(比如当前示例中的 `input`元素),在 `constructor` 中使用 `React.createRef()` 创建的 `ref`会接收 **来自底层`DOM`元素的 `current`值**。 > 译注:这里的 `current` 应该是[合成事件(SyntheticEvent)](http://react.html.cn/docs/events.html) 这意味着访问 `DOM` 值,我们需要写这样的东西: ``` this.textInput.current; ``` ## Refs 回调 **Refs 回调** 是在 React 中使用 `ref` 的另一种方式。要以这种方式使用 ref,我们需要为 ref 属性设置回调函数。 当我们设置 ref 时,React 会调用这个函数,并将 element 作为第一个参数传递给它。 ``` // Refs.js class CustomTextInput extends React.Component { constructor(props) { super(props); this.textInput = null; // 回调,传入的 this.setTextInputRef = element => { this.textInput = element; }; } handleSubmit = e => { e.preventDefault(); console.log(this.textInput.value); }; render() { return ( <div> <form onSubmit={e => this.handleSubmit(e)}> <input type="text" ref={this.setTextInputRef} /> <button>Submit</button> </form> </div> ); } ``` 上面的示例中,我们将 input 标签的 `ref` 设置为 `this.setTextInputRef`。 * 当组件安装时,React 会将 DOM 元素传递给 ref 的回调; * 当组件卸载时,则会传递 `null`。 (`ref` 回调会在 `componentDidMount` 和 `componentDidUpdate` 生命周期之前调用。) ## `React.forwardRef` 不能在函数组件上使用`ref`属性,因为函数组件没有实例。 如果您希望引用函数组件,您可以使用`forwardRef`可能与`useImperativeHandle`结合使用),或者您可以将它转换为类组件。 ``` // Ref.js // 普通的函数组件 是不会有 ref 参数的,React.forwardRef 返回一个组件 const TextInput = React.forwardRef((props, ref) => ( <input type="text" placeholder="Hello World" ref={ref} /> )); const inputRef = React.createRef(); class CustomTextInput extends React.Component { handleSubmit = e => { e.preventDefault(); console.log(inputRef.current.value); }; render() { return ( <div> <form onSubmit={e => this.handleSubmit(e)}> <TextInput ref={inputRef} /> <button>Submit</button> </form> </div> ); } } ``` `Ref forwarding`允许组件接收一个`ref`,并将它向下传递(换句话说,“转发”它)给子组件。 在上面的示例中,我们使用`input`标签创建了一个名为`TextInput`的组件。那么,我们如何将`ref`传递或转发到`input`标签呢? 首先,我们使用下面的代码创建一个`ref`: ``` const inputRef = React.createRef(); ``` 然后,我们**通过组件 `<TextInput ref={inputRef}>` 的 `ref` 属性的值,将 `ref` 向下传递**。然后`React` 将会把 `ref` 作为第二个参数转发给 `forwardRef` 函数(这个是在 React 框架层面完成的事情)。 接下来,我们将此 `ref` 参数转发给`<input ref={ref}>`。现在可以在外层组件通过 `inputRef.current` 访问 DOM 节点的值了。 ## 高阶组件的 ref 如果我要操作一个高阶组件 的 DOM ,怎么办? ref 是不能像 props 一样,往下面传递的,因此想要往下面传递,必须要用到`React.forwardRef`这个 API。 ``` // by 司徒正美 const ThemeContext = React.createContext('light'); class ThemeProvider extends React.Component { state = {theme: 'light'}; render() { return ( <ThemeContext.Provider value={this.state.theme}> {this.props.children} </ThemeContext.Provider> ); } } class FancyButton extends React.Component { buttonRef = React.createRef(); focus() { this.buttonRef.current.focus(); } render() { const {label, theme, ...rest} = this.props; return ( <button {...rest} className={`${theme}-button`} ref={this.buttonRef}> {label} </button> ); } } function withTheme(Component) { // React.forwardRef 会提供 第二个参数"ref",然后就可以直接把其附加到组件上 function ThemedComponent(props, ref) { return ( <ThemeContext.Consumer> {theme => ( <Component {...props} ref={ref} theme={theme} /> )} </ThemeContext.Consumer> ); } // These next lines are not necessary, // But they do give the component a better display name in DevTools, // e.g. "ForwardRef(withTheme(MyComponent))" const name = Component.displayName || Component.name; ThemedComponent.displayName = `withTheme(${name})`; // 告诉 React 传递 "ref" 到 ThemedComponent. return React.forwardRef(ThemedComponent); } const fancyButtonRef = React.createRef(); const FancyThemedButton = withTheme(FancyButton); // fancyButtonRef 现在指向 FancyButton <FancyThemedButton label="Click me!" onClick={handleClick} ref={fancyButtonRef} />; ``` # 结论 与通过`props`和`state`不同,`Refs`是一种将数据传递给特定子实例的好方法。 你必须要小心,因为`refs`操纵实际的`DOM`,而不是虚拟的`DOM`,这与`React`思维方式相矛盾。因此,虽然`refs`不应该是通过应用程序流动数据的默认方法,但是当您需要时,它们是可以从`DOM`元素读取数据的好方法。 # 参考 推荐下「司徒正美」大佬的[React v16.3.0: New lifecycles and context API](https://segmentfault.com/a/1190000014083970),createRef API,forwardRef API 中的示例可以作为补充阅读。 [https://segmentfault.com/a/1190000019277029](https://segmentfault.com/a/1190000019277029)