在 React 程序中,一般会使用 ref
获取 DOM 元素。例如:
constructor(){
super();
// 创建 ref
this.divRef = React.createRef();
}
componentDidMount(){
// DOM 元素可以通过 current 属性获得
console.log(this.divRef.current);
}
render(){
// 使用 ref
return <div ref={this.divRef}>123</div>
}
使用 refs
的几个场景:
在 React Hook
中可以使用 useRef
创建一个 ref
。例如:
function App(){
let divRef = useRef();
useEffect(() => {
// 渲染完成后获取 DOM 元素
console.log(divRef.current);
},[]);
return (
<div ref={divRef}>
123
</div>
);
}
useRef
还可以传入一个初始值,这个值会保存在 ref.current
中,上面代码中,如果不给 div 元素传递 ref={divRef}
,则 divRef.current
的值将是我们传入的初始值。
useRef
和 createRef
并没有什么区别,只是 createRef
用在类组件当中,而 useRef
用在 Hook 组件当中。在类组件中,可以在类的实例上存放内容,这些内容随着实例化产生或销毁。但在 Hook 中,函数组件并没有 this(组件实例),因此 useRef
作为这一能力的弥补。在组件重新渲染时,返回的 ref 对象在组件的整个生命周期内保持不变。变更 ref 对象中的 .current
属性不会引发组件重新渲染。比如下面函数组件的例子:
function App(){
let uRef = useRef(1);
let [count, setCount] = useState(uRef.current);
const handleClick = useCallback(() => {
uRef.current += 1; // current 值加一
setCount(uRef.current);
},[]);
return (
<div>
<h1>useRef: { count }</h1>
<button onClick={handleClick}>Click!</button>
</div>
);
}
上面代码中,每次点击按钮 uRef.current
就会加一,并更新 count
值。count
值会一直累加,如果把 h1
中的 count
换成 uRef.current
,组件并不会更新。当然,如果给 useCallback
的数组中添加 uRef.current
,让它监听其变化,那还是会更新的,但不应这么做。这就失去了 ref 的意义。
不要在 Hook 组件(或者函数组件)中使用 createRef
,它没有 Hook 的功能,函数组件每次重渲染,createRef
就会生成新的 ref 对象。使用类组件实现上面 Hook 一样的功能:
class App extends Component{
constructor(){
super();
this.state = {
count: 1
};
this.divRef = React.createRef();
this.divRef.current = 1; // 初始化
this.handleClick = this.handleClick.bind(this);
}
handleClick(){
this.divRef.current += 1;
this.setState({
count: this.divRef.current
});
}
render () {
return (
<div>
<h1>Hello! {this.state.count}</h1>
<button onClick={this.handleClick}>Click</button>
</div>
);
}
}
在 createRef
和 useRef
出现之前,可以使用回调的方式使用 ref 获取 DOM,例如:
class App extends Component{
constructor(){
super();
this.iptRef = null;
}
componentDidMount(){
// 组件挂载完成后,输入框自动对焦
this.iptRef.focus();
}
render () {
return (
<div>
<input type="text" ref={ipt => this.iptRef = ipt} />
</div>
);
}
}
上面代码中,元素的 ref
接受一个函数,函数的参数就是 DOM 节点,然后把节点赋给组件实例的 iptRef
。
上面介绍了如何在 DOM 元素上使用 ref,ref 还可以获取组件实例。例如:
class Counter extends Component{
constructor(){
super();
this.state = {
count: 1
}
this.increment = this.increment.bind(this);
}
increment(){
this.setState({
count: this.state.count + 1
});
}
render(){
return (
<div>
<h1>count: {this.state.count}</h1>
</div>
);
}
}
class App extends Component{
constructor(){
super();
this.handleClick = this.handleClick.bind(this);
}
handleClick () {
this.counterIntance.increment();
}
render () {
return (
<div>
<Counter ref={obj => this.counterIntance = obj} />
<button onClick={this.handleClick}>Click</button>
</div>
);
}
}
在 App 组件中,Counter
子组件使用 ref
获取其实例对象,父组件用 counterIntance
属性接收。当点击按钮时会调用 Counter
组件上的 increment
方法。
例如:
function Input(props){
return (
<input type="text" ref={ props.iptRef } />
);
}
class App extends Component{
componentDidMount(){
console.log(this.iptElm);
}
render () {
return (
<div>
<Input iptRef={el => this.iptElm = el} />
</div>
);
}
}
将父组件的 iptRef 状态(是一个 ref 回调形式的函数)传递给子组件,父组件中的 iptElm
就可以接收到 DOM 元素了。如果不使用 Hook
,在函数组件中是无法操作 DOM 的,一个办法就是写成类组件形式,或者将 DOM 元素传递给父组件(父组件应是一个类组件)。除了使用这种方式外,也可以使用 React 提供的 forwardRef
API。比如:
// 使用 forwardRef 包裹后,函数组件的第二个参数将是,父组件传入的 ref 对象
const Input = React.forwardRef((props, iptRef) => {
return (
<input type="text" ref={iptRef} />
);
});
class App extends Component{
constructor(){
super();
// 创建 ref 对象
this.iptRef = createRef();
}
componentDidMount(){
// 将会打印出 input 元素
console.log(this.iptRef.current);
}
render () {
return (
<div>
{</* 将 ref 对象传入子组件当中 */}
<Input ref={this.iptRef} />
</div>
);
}
}
对于高阶组件(HOC),可以利用 forwardRef
实现父组件获取子组件 DOM 元素。例如:
function withComp(WrapperComponent){
class Example extends Component{
render(){
const { forwardRef, ...rest } = this.props;
return <WrapperComponent ref={forwardRef} {...rest} />
}
}
return React.forwardRef((props, ref) => {
return <Example {...props} forwardRef={ref} />
});
}
withComp
是一个高阶组件,它会返回 forwardRef
包裹的函数组件,这个函数组件内部直接返回 Example
类组件,使用 forwardRef
属性接收到从父组件传来的 ref
对象。Example
组件中就可以接收到函数组件传递来的 forwardRef
属性,然后 WrapperComponent
相当于父组件,我们自己写的子组件需要使用 forwardRef
包一层。例如:
const Child = React.forwardRef((props, forwardRef) => {
return (
<div>
<h1>{props.msg}</h1>
{/* forwardRef 是父组件传来的 ref 对象 */}
<input ref={forwardRef} type="text" />
</div>
);
});
// 增强组件
const Input = withComp(Child);
class App extends Component{
constructor(){
super();
this.state = {
msg: 'Hello',
};
this.iptRef = createRef();
}
componentDidMount(){
// 获取到 input 元素
console.log(this.iptRef.current);
}
render(){
return (
<div>
<Input ref={ this.iptRef } msg={ this.state.msg } />
</div>
);
}
}
如果你不想用 React.forwardRef
“包一层”,也可以交由高阶组件来完成,把 withComp
中的代码改一行即可:
class Example extends Component{
render(){
const { forwardRef, ...rest } = this.props;
// forwardRef 作为属性传给 WrapperComponent(Child组件)
return <WrapperComponent forwardRef={forwardRef} {...rest} />
}
}
把 ref
对象传递给 WrapperComponent
组件。这样,我们在子组件中使用 ref
时直接使用即可:
function Child(props) {
// 此时父组件传来的 ref 对象在 props 中
// 不好的一点是,只能使用 props.forwardRef 获取
// 这可能会出现问题:父组件中传入的就有 forwardRef 属性,
// 值就会被覆盖或者获取到的不是 ref 对象
return (
<div>
<h1>{props.msg}</h1>
{/* 从 props 中取出 forwardRef,即 ref 对象 */}
<input ref={props.forwardRef} type="text" />
</div>
);
};
const Input = withComp(Child);
// ....
useRef
除了访问 DOM
节点外,useRef
还可以有别的用处,你可以把它看作类组件中声明的实例属性,属性可以存储一些内容,内容改变不会触发视图更新。以一个计时器的例子了解 useRef
的用法。
Demo 描述:一个 100ms 的计时器,当点击 Start 按钮时就会计时,点击 End 按钮时停止计时,如何实现?
如果使用类组件,可以这么做:
class App extends React.Component{
constructor(){
super();
this.state = {
count: 0
};
// 用于接收定时器 ID
this.timer = undefined;
this.startHandler = this.startHandler.bind(this);
this.endHandler = this.endHandler.bind(this);
}
startHandler(){
if(!this.timer){ // 如果定时器没有值时才去赋值,不然多次点击按钮会设置多个定时器
this.timer = setInterval(() => {
this.setState({
count: this.state.count + 1
});
},100);
}
}
endHandler(){
clearInterval(this.timer);
// 把定时器设置成假值
this.timer = undefined;
}
render(){
return (
<div>
<h2>{this.state.count}</h2>
<button onClick={this.startHandler}>Start!</button>
<button onClick={this.endHandler}>Stop!</button>
</div>
);
}
}
在类组件中,可以定义一个 timer
属性用于接收定时器 ID,但在函数组件中并没有 this(组件实例),这就要借助到 useRef
。代码如下:
function App(){
let [count, setCount] = useState(0);
const timer = useRef(null);
const start = useCallback(() => {
if(timer.current) return;
timer.current = setInterval(() => {
setCount(count => count + 1);
},100);
},[]);
const stop = useCallback(() => {
clearInterval(timer.current);
timer.current = null;
},[]);
return (
<div>
<h2>{count}</h2>
<button onClick={start}>Start!</button>
<button onClick={stop}>Stop!</button>
</div>
);
}
可以看到,使用函数组件要比类组件书写简洁许多。
再看一个例子,实现一个下面动图这样的功能,输入框输入的数字相当于计时器的毫秒延迟,当输入框数值变化时计时器会做相应的调整。如何实现?
显然,我们需要两个状态,一个是 count,表示数字的变化;另一个是 delay,延迟时间会随着输入值不不同而变化。代码如下:
function App(){
let [count, setCount] = useState(0);
let [delay, setDelay] = useState(1000);
const timer = useRef(null);
useEffect(() => {
if(!timer.current){
timer.current = setInterval(() => {
setCount(count => count + 1);
},delay);
}
return () => {
clearInterval(timer.current);
timer.current = null;
}
},[delay]);
const handleChange = useCallback((event) => {
setDelay(event.target.value);
},[]);
return (
<div>
<h2>{count}</h2>
<input type="number" value={delay} onChange={handleChange} />
</div>
);
}
我们可以把中间的 useEffect
部分抽离出来,自定义一个 Hook:useInterval
。代码如下:
const useInterval = (callback, delay) => {
const savedCallback = useRef();
useEffect(() => { // callback 变更时重新赋值
savedCallback.current = callback;
},[callback]);
useEffect(() => {
// 每次 delay 变化(重渲染),都应生成一个新的计时器回调
// 这样计时器的回调函数才会引用新的 props 和 state
const handler = () => savedCallback.current();
if(delay !== null){
const id = setInterval(handler, delay);
return () => clearInterval(id); // 别忘了清除计时器
}
},[delay]);
}
function App(){
let [count, setCount] = useState(0);
let [delay, setDelay] = useState(1000);
useInterval(function(){
setCount(count + 1);
},delay);
const handleChange = useCallback((event) => {
setDelay(event.target.value);
},[]);
return (
<div>
<h2>{count}</h2>
<input type="number" value={delay} onChange={handleChange} />
</div>
);
}
关于 useInterval
介绍可以参考这篇文章:使用 React Hooks 声明 setInterval[1]
如果一个表单元素的值是由 React 控制,就其称为受控组件。比如 input 框的 value 由 React 状态管理,当 change 事件触发时,改变状态。而非受控组件就像是运行在 React 体系之外的表单元素,当用户将数据输入到表单字段(例如 input,dropdown 等)时,React 不需要做任何事情就可以映射更新后的信息,非受控组件可能就要手动操作 DOM 元素(使用 React 中的 ref 获取元素),input 中使用 defaultValue
取代 value
属性,defaultChecked
代替 checked
属性。例如下面的代码就是一个非受控组件。
class App extends React.Component{
constructor(){
super();
this.iptRef = React.createRef();
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(event){
// 提交时获取到输入框的值
console.log(this.iptRef.current.value);
// ... 做一些表单信息的验证操作
event.preventDefault();
}
render(){
return (
<form onSubmit={this.handleSubmit}>
<input type="text" defaultValue="" ref={this.iptRef} />
<input type="submit" value="提交" />
</form>
)
}
}
[1]
使用 React Hooks 声明 setInterval: https://overreacted.io/zh-hans/making-setinterval-declarative-with-react-hooks/