关于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
推荐阅读
我的公众号能带来什么价值?(文末有送书规则,一定要看)
每个前端工程师都应该了解的图片知识(长文建议收藏)
为什么现在面试总是面试造火箭?
浏览
28