React之Ref

什么是refs

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。

在组件mount之后再去获取refcomponentWillMount和第一次render时都获取不到,在componentDidMount才能获取到

refs的使用方式

类组件
  • 字符串(string ref) React v16.3 之前

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // string ref
    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
    // callback ref
    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
    // React.createRef
    class MyComponent extends React.Component {
    constructor(props) {
    super(props);
    this.myRef = React.createRef();
    }
    componentDidMount() {
    this.myRef.current.focus();
    }
    render() {
    return <input ref={this.myRef} />;
    }
    }
函数组件
  • useRef

    1
    const refContainer = useRef(initialValue);

    useRef返回一个可变的ref对象,其.current属性被初始化为传入的参数(initialValue)。返回的ref对象在整个生命周期内保持不变。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// 关键代码,`current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

先通过useRef创建一个ref对象inputEl,点击button,然后再将inputEl赋值给inputref,最后,通过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 = () => {
// 关键代码,`current` 指向已挂载到 DOM 上的文本输入元素
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了。

有时候,我们可能不想将整个子组件暴露给父组件,而只是暴露出父组件需要的值或者方法,这样可以让代码更加明确。而useImperativeHandleApi就是帮助我们做这件事的。

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 = () => {
// 关键代码,`current` 指向已挂载到 DOM 上的文本输入元素
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 = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
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代替了useRefcallback ref会将当前ref的值变化通知到父组件。

场景

  • 对input/video/audio需要控制时,例如输入框焦点、媒体播放状态
  • 触发强制动画
  • 集成第三方库(传递 dom 节点进去)

注意:如果能使用props实现,应该尽量避免使用refs实现

ref拿到的到底是什么

很多文章里面说ref拿到的是真实DOM节点。其实这种说法很笼统。也很让人困惑,上面看到拿到的要么是实例(我们自定义组件)要么是component的_hostNode属性,这个好像不是真实DOM

  • 什么是_hostNode?

是通过document.createElement方法创建的element对象。只是这时候对象保存在了虚拟DOM中,然后再塞入真实DOM树。所以说_hostNode和真实DOM树中的DOM的关系就是不同对象的不同属性指向的同一块存储空间,引用着同一个值而已。

虽然是通过虚拟DOM的_hostNode拿到这个值,但是对他的操作会体现在真实DOM节点上。说白了就是对象的引用赋值。
所以,ref拿到的是真实DOM的引用这个说法更准确。

参考:源码看React—- ref