乐趣区

关于javascript:ref的学习

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>
));

// 父组件中应用子组件的 ref
function 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>
  )
}
退出移动版