关于ref的一切
作为React
开发者,你能回答如下几个问题么?
- 为什么
string
类型的ref prop
将会被废弃? -
function
类型的ref prop
会在什么时机被调用? -
React.createRef
与useRef
的返回值有什么不同?
其实,这三个问题中的ref
包含两个不同概念:
- 不管是
string
、function
类型或是React.createRef
、useRef
创建的ref
,都是作为数据结构
看待 - 问题2探讨的时机是将
ref
作为生命周期
看待
接下来本文会分别从数据结构
、生命周期
两个角度探讨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 <input ref={'input-' + index} />;
// 如果使用function类型ref,则不会有这个问题
// return <input ref={input => this['input-' + index] = input} />;
}
render() {
return <DataTable data={this.props.data} renderRow={this.renderRow} />
}
}
还有其他原因使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
的数据结构
。
ref的生命周期
在React
中,HostComponent
、ClassComponent
、ForwardRef
可以赋值ref
属性。
这个属性在ref
生命周期的不同阶段会被执行(对于function
)或赋值(对于{current: any}
)。
// HostComponent
<div ref={domRef}></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
:
<div ref={dom => 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
- (收藏)搭建.NET Framework 3.0开发环境 及SharePoint 2007/WSS 3环境
- WCF技术剖析之八:ClientBase<T>中对ChannelFactory<T>的缓存机制
- ASP.NET MVC5+EF6+EasyUI 后台管理系统(48)-工作流设计-起草新申请
- 把windows2003“搬”到手机上。
- ASP.NET MVC5+EF6+EasyUI 后台管理系统(53)-工作流设计-我的批阅
- ASP.NET MVC5+EF6+EasyUI 后台管理系统--任务调度系统解析
- ASP.NET MVC5+EF6+EasyUI 后台管理系统(52)-美化EasyUI皮肤和图标
- ASP.NET MVC5+EF6+EasyUI 后台管理系统--系统权限全套完整图
- 互联网时代的产品升级和用户反馈故事
- ASP.NET MVC5+EF6+EasyUI 后台管理系统(50)-Easyui 扁平化皮肤
- 微信终于做了它最擅长的业务,市场已经轰动
- Bootstrap Metronic 学习记录(二)菜单栏
- WCF技术剖析之十一:异步操作在WCF中的应用(上篇)
- Bootstrap Metronic 学习记录(一)简介
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 面试官:spring的BeanFatory和FactoryBean区别
- Saltstack_使用指南06_远程执行-指定目标
- 什么是缓存击穿、雪崩、穿透
- 如何卸载CDH7.1.1
- java8 stream的这些开发技巧,你值得好好收藏
- 面试官:mybatis中#{ }和${ }的区别
- python 学习笔记(9)——Python 正则表达式
- 万能的BeanPostProcessor是如何让spring无限扩展的?
- spring解决循环依赖为什么要用三级缓存?
- 深入剖析ThreadLocal
- spring事务的这10种坑,你稍不注意可能就会踩中!!!
- 面试前看了这篇spring事务的文章,让我多要了2k的工资
- 面试时被问到单例模式,怎么回答才能让面试官眼前一亮?
- 老司机手把手教你编写自己的springboot starter
- 实战|如何消除又臭又长的if...else判断更优雅的编程?