关于ref的一切
作为React开发者,你能回答如下几个问题么?
为什么
string类型的ref prop将会被废弃?function类型的ref prop会在什么时机被调用?React.createRef与useRef的返回值有什么不同?
其实,这三个问题中的ref包含两个不同概念:
不管是
string、function类型或是React.createRef、useRef创建的ref,都是作为数据结构看待问题2探讨的时机是将
ref作为生命周期看待
接下来本文会分别从数据结构、生命周期两个角度探讨ref。
这,就是关于ref的一切。
为什么string类型的ref prop将会被废弃?
string类型的ref使用方式如下:
点击input标签会打印input的value。
class Foo extends Component {
  render() {
    return (
      <input
        onClick={() => this.action()} 
        ref='input' 
      />
    );
  }
  action() {
    console.log(this.refs.input.value);
  }
}
string类型ref prop最主要的两个问题是:
- 由于是
string的写法,无法直接获得this的指向。 
所以,React需要持续追踪当前render的组件。这会让React在性能上变慢。
- 当使用
render回调函数的开发模式,获得ref的组件实例可能与预期不同。 
比如:
class App extends React.Component {
  renderRow = (index) => {
    // ref会绑定到DataTable组件实例,而不是App组件实例上
    return ;
    // 如果使用function类型ref,则不会有这个问题
    // return  this['input-' + index] = input} />;
  }
 
  render() {
    return 
  }
}
还有其他原因使React团队决定在未来放弃string Ref,详见#1373[1]与#8333[2]。
React.createRef
我们直接看React.createRef的源码:
function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  return refObject;
}
可见,ref对象就是仅仅是包含current属性的普通对象。
useRef
为了验证这个观点,我们再看useRef的源码。
对于mount与update,useRef分别对应两个函数。
对于
hook如何保存数据如果不了解,可以看本系列第一篇文章关于useState的一切
function mountRef<T>(initialValue: T): {|current: T|} {
  // 获取当前useRef hook
  const hook = mountWorkInProgressHook();
  // 创建ref
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}
function updateRef<T>(initialValue: T): {|current: T|} {
  // 获取当前useRef hook
  const hook = updateWorkInProgressHook();
  // 返回保存的数据
  return hook.memoizedState;
}
可以看到,ref对象确实仅仅是包含current属性的对象。
function ref
除了{current: any}类型外,ref还能作为function。
作为function时,仅仅是在不同生命周期阶段被调用的回调函数。
在我们接下来的讨论中,只涉及function | {current: any}这两种ref的数据结构。
在React中,HostComponent、ClassComponent、ForwardRef可以赋值ref属性。
这个属性在ref生命周期的不同阶段会被执行(对于function)或赋值(对于{current: any})。
// HostComponent
div>
// ClassComponent / ForwardRef
<App ref={cpnRef} />
其中,ForwardRef只是将ref作为第二个参数传递下去,没有别的特殊处理。
// 对于ForwardRef,secondArg为传递下去的ref
const children = forwardRef(
  (props, secondArg) => {
    //render逻辑...
  }
);
所以接下来讨论ref的生命周期时不会单独讨论ForwardRef。
在本系列文章中我们讲过,React的渲染包含两个阶段:
render阶段:为需要更新的组件对应fiber打上标签(effectTag)
commit阶段:执行effectTag对应更新操作
// 部分effectTag定义
// 插入DOM
export const Placement = /* */ 0b0000000000000010;
// 更新DOM的属性
export const Update = /*    */ 0b0000000000000100;
// 删除DOM
export const Deletion = /*  */ 0b0000000000001000;
// 有ref操作
export const Ref = /*       */ 0b0000000010000000;
// ...
对于HostComponent、ClassComponent如果包含ref操作,那么也会赋值相应的effectTag。
同其他effectTag对应操作的执行一样,ref的更新也是发生在commit阶段。
所以,ref的生命周期可以分为两个大阶段:
render阶段为含有ref属性的Component对应fiber添加Ref effectTag
commit阶段为包含Ref effectTag的fiber执行对应操作
render阶段
在render阶段,组件对应fiber被赋值Ref effectTag需要满足的条件:
fiber类型为HostComponent、ClassComponent、ScopeComponent
ScopeComponent是一种用于管理focus的测试特性,这种情况我们不讨论。详见PR[3]
对于mount,workInProgress.ref !== null,即组件首次render时存在ref属性
对于update,current.ref !== workInProgress.ref,即组件更新时ref属性改变
commit阶段
在commit阶段,ref的生命周期分为两个子阶段:
移除之前的ref
更新ref
移除之前的ref
- 对于
ref属性改变的情况,需要先移除之前的ref。 
调用的是commitDetachRef:
function commitDetachRef(current: Fiber) {
  const currentRef = current.ref;
  if (currentRef !== null) {
    if (typeof currentRef === 'function') {
      // function类型ref,调用他,传参为null
      currentRef(null);
    } else {
      // 对象类型ref,current赋值为null
      currentRef.current = null;
    }
  }
}
可以看到,function与{current: any}类型的ref的生命周期并没有什么不同,只是一种会被调用,一种会被赋值。
- 对于
Deletion effectTag的fiber(对应需要删除的DOM节点),需要递归他的子树,对子孙fiber的ref执行类似commitDetachRef的操作。 
更新ref
接下来进入ref的更新阶段。
执行这一步的操作叫commitAttachRef:
function commitAttachRef(finishedWork: Fiber) {
  // finishedWork为含有Ref effectTag的fiber
  const ref = finishedWork.ref;
  
  // 含有ref prop,这里是作为数据结构
  if (ref !== null) {
    // 获取ref属性对应的Component实例
    const instance = finishedWork.stateNode;
    let instanceToUse;
    switch (finishedWork.tag) {
      case HostComponent:
        // 对于HostComponent,实例为对应DOM节点
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        // 其他类型实例为fiber.stateNode
        instanceToUse = instance;
    }
    // 赋值ref
    if (typeof ref === 'function') {
      ref(instanceToUse);
    } else {
      ref.current = instanceToUse;
    }
  }
}
可以看到,对于包含ref属性的fiber,针对ref的不同类型,执行调用/赋值操作。
至此,ref的生命周期完成。
总结
通过本文我们学习了ref的数据结构及生命周期。
对于赋值了ref属性的HostComponent与ClassComponent,他会依次经历:
在render阶段赋值Ref effectTag
如果ref变化,在commit阶段会先删除之前的ref。
接下来,会进入ref的更新流程。
所以,对于内联函数的ref:
 this.dom = dom}>div>
由于每次render ref都对应一个全新的内联函数,所以在commit阶段会先执行commitDetachRef删除再执行commitAttachRef更新。
即内联函数会被调用两次,第一次传参dom的值为null,第二次为更新的DOM。
参考资料
[1]#1373: https://github.com/facebook/react/issues/1373
[2]#8333: https://github.com/facebook/react/pull/8333#issuecomment-271648615
[3]PR: https://github.com/facebook/react/pull/16587
推荐阅读
我的公众号能带来什么价值?(文末有送书规则,一定要看)
每个前端工程师都应该了解的图片知识(长文建议收藏)
为什么现在面试总是面试造火箭?
浏览
                    53