众所周知,React 经过声明式的渲染机制把复杂的 DOM 操做抽象成为简单的 state 与 props 操做,一时圈粉无数,一晚上间将前端工程师从面条式的 DOM 操做中拯救出来。尽管咱们一再强调在 React 开发中尽可能避免 DOM 操做,但在一些场景中仍然没法避免。固然 React 并无把路堵死,它提供了 ref 用于访问在 render 方法中建立的 DOM 元素或者是 React 组件实例。javascript
在 React v16.3 以前,ref 经过字符串(string ref)或者回调函数(callback ref)的形式进行获取,在 v16.3 中,经 0017-new-create-ref 提案引入了新的 React.createRef API。前端
注意:本文如下代码示例以及源码均基于或来源于 React v16.3.2 release 版本。java
// string ref
class MyComponent extends React.Component {
componentDidMount() {
this.refs.myRef.focus();
}
render() {
return <input ref="myRef" />;
}
}
// callback ref
class MyComponent extends React.Component {
componentDidMount() {
this.myRef.focus();
}
render() {
return <input ref={(ele) => {
this.myRef = ele;
}} />;
}
}
// 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} />;
}
}
复制代码
在 React.createRef 出现以前,string ref 就已被诟病已久,React 官方文档直接提出 string ref 将会在将来版本被移出,建议用户使用 callback ref 来代替,为什么须要这么作呢?主要缘由集中于如下几点:react
function coerceRef( returnFiber: Fiber, current: Fiber | null, element: ReactElement, ) {
...
const stringRef = '' + element.ref;
// 从 fiber 中获得实例
let inst = ownerFiber.stateNode;
// ref 闭包函数
const ref = function(value) {
const refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs;
if (value === null) {
delete refs[stringRef];
} else {
refs[stringRef] = value;
}
};
ref._stringRef = stringRef;
return ref;
...
}
复制代码
class MyComponent extends Component {
renderRow = (index) => {
// string ref 会挂载在 DataTable this 上
return <input ref={'input-' + index} />;
// callback ref 会挂载在 MyComponent this 上
return <input ref={input => this['input-' + index] = input} />;
}
render() {
return <DataTable data={this.props.data} renderRow={this.renderRow} />
}
}
复制代码
/** string ref **/
class Parent extends React.Component {
componentDidMount() {
// 可获取到 this.refs.childRef
console.log(this.refs);
}
render() {
const { children } = this.props;
return React.cloneElement(children, {
ref: 'childRef',
});
}
}
class App extends React.Component {
componentDidMount() {
// this.refs.child 没法获取到
console.log(this.refs);
}
render() {
return (
<Parent>
<Child ref="child" />
</Parent>
);
}
}
/** callback ref **/
class Parent extends React.Component {
componentDidMount() {
// 能够获取到 child ref
console.log(this.childRef);
}
render() {
const { children } = this.props;
return React.cloneElement(children, {
ref: (child) => {
this.childRef = child;
children.ref && children.ref(child);
}
});
}
}
class App extends React.Component {
componentDidMount() {
// 能够获取到 child ref
console.log(this.child);
}
render() {
return (
<Parent>
<Child ref={(child) => {
this.child = child;
}} />
</Parent>
);
}
}
复制代码
ReactDOM.render(<App ref="app" />, document.getElementById('main')); 复制代码
对于静态类型较不友好,当使用 string ref 时,必须显式声明 refs 的类型,没法完成自动推导。git
编译器没法将 string ref 与其 refs 上对应的属性进行混淆,而使用 callback ref,可被混淆。github
对比新的 createRef 与 callback ref,并无压倒性的优点,只是但愿成为一个便捷的特性,在性能上会会有微小的优点,callback ref 采用了组件 render 过程当中在闭包函数中分配 ref 的模式,而 createRef 则采用了 object ref。前端工程师
createRef 显得更加直观,相似于 string ref,避免了 callback ref 的一些理解问题,对于 callback ref 咱们一般会使用内联函数的形式,那么每次渲染都会从新建立,因为 react 会清理旧的 ref 而后设置新的(见下图,commitDetachRef -> commitAttachRef),所以更新期间会调用两次,第一次为 null,若是在 callback 中带有业务逻辑的话,可能会出错,固然能够经过将 callback 定义成类成员函数并进行绑定的方式避免。闭包
class App extends React.Component {
state = {
a: 1,
};
componentDidMount() {
this.setState({
a: 2,
});
}
render() {
return (
<div ref={(dom) => { // 输出 3 次 // <div data-reactroot></div> // null // <div data-reactroot></div> console.log(dom); }}></div>
);
}
}
class App extends React.Component {
state = {
a: 1,
};
constructor(props) {
super(props);
this.refCallback = this.refCallback.bind(this);
}
componentDidMount() {
this.setState({
a: 2,
});
}
refCallback(dom) {
// 只输出 1 次
// <div data-reactroot></div>
console.log(dom);
}
render() {
return (
<div ref={this.refCallback}></div>
);
}
}
复制代码
不过不得不认可,createRef 在能力上仍逊色于 callback ref,例如上一节提到的组合问题,createRef 也是无能为力的。在 React v16.3 中,string ref/callback ref 与 createRef 的处理略有差异,让咱们来看一下 ref 整个构建流程。app
// markRef 前会进行新旧 ref 的引用比较
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
// effectTag 基于位操做,其中有 ref 的变动标志位
function markRef(workInProgress: Fiber) {
workInProgress.effectTag |= Ref;
}
// effectTag 与 Ref 的 & 操做表示当前 fiber 有 ref 变动
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
// 当前 Host 环境为 DOM 环境,HostComponent 即为 DOM 元素,须要借助实例获取原生 DOM 元素
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
// 对于 ClassComponent 等而言,直接返回实例便可
default:
instanceToUse = instance;
}
// string ref 与 callback 都会去执行 ref 闭包函数
// createRef 会直接挂在 object ref 的 current 上
if (typeof ref === 'function') {
ref(instanceToUse);
} else {
ref.current = instanceToUse;
}
}
}
复制代码
以上会涉及 react fiber 的一些概念与细节,好比:fiber 对象含义,fiber tree 构建更新过程,effectTag 的含义与收集过程等等,若是读者对上述细节不熟悉,可暂时跳过此段内容,不影响对于 ref 的掌握与理解。dom
除了 createRef 之外,React16 还另外提供了一个关于 ref 的 API React.forwardRef,主要用于穿过父元素直接获取子元素的 ref。在提到 forwardRef 的使用场景以前,咱们先来回顾一下,HOC(higher-order component)在 ref 使用上的问题,HOC 的 ref 是没法经过 props 进行传递的,所以没法直接获取被包裹组件(WrappedComponent),须要进行中转。
function HOCProps(WrappedComponent) {
class HOCComponent extends React.Component {
constructor(props) {
super(props);
this.setWrappedInstance = this.setWrappedInstance.bind(this);
}
getWrappedInstance() {
return this.wrappedInstance;
}
// 实现 ref 的访问
setWrappedInstance(ref) {
this.wrappedInstance = ref;
}
render() {
return <WrappedComponent ref={this.setWrappedInstance} {...this.props} />;
}
}
return HOCComponent;
}
const App = HOCProps(Wrap);
<App ref={(dom) => {
// 只能获取到 HOCComponent
console.log(dom);
// 经过中转后能够获取到 WrappedComponent
console.log(dom.getWrappedInstance());
}} />
复制代码
在拥有 forwardRef 以后,就不须要再经过 getWrappedInstance 了,利用 forwardRef 能直接穿透 HOCComponent 获取到 WrappedComponent。
function HOCProps(WrappedComponent) {
class HOCComponent extends React.Component {
render() {
const { forwardedRef, ...rest } = this.props;
return <WrappedComponent ref={forwardedRef} {...rest} />;
}
}
return React.forwardRef((props, ref) => {
return <HOCComponent forwardedRef={ref} {...props} />;
});
}
const App = HOCProps(Wrap);
<App ref={(dom) => {
// 能够直接获取 WrappedComponent
console.log(dom);
}} />
复制代码
React.forwardRef 的原理其实很是简单,forwardRef 会生成 react 内部一种较为特殊的 Component。当进行建立更新操做时,会将 forwardRef 组件上的 props 与 ref 直接传递给提早注入的 render 函数,来生成 children。
const nextChildren = render(workInProgress.pendingProps, workInProgress.ref);
复制代码
React refs 到此就所有介绍完了,在 React16 新版本中,新引入了 React.createRef 与 React.forwardRef 两个 API,有计划移除老的 string ref,使 ref 的使用更加便捷与明确。若是你的应用已经升级到 React16.3+ 版本,那就放心大胆使用 React.createRef 吧,若是暂时没有的话,建议使用 callback ref 来代替 string ref。
咱们团队目前正在深刻研究 React16,欢迎社区小伙伴和咱们一块儿探讨与前行,若是想加入咱们,欢迎私聊或投递简历到 dancang.hj@alibaba-inc.com。