从createRef方法来理解js的内存操作
前情提要
在逛技术社区的过程中遇到这么一个问题:为什么获取createRef的结果需要多一层current呢?再回答完题主的问题后,便决定水一篇文章。
注:内容比较简单,如果喜欢点个赞,不喜勿喷。欢迎在评论区内交流。如果本人观点有错误的地方,恳请大佬多多指点,感谢🙏。
再注:文中所有观点都是我个人理解,没有查阅ECMAScript官方文档。欢迎大佬在评论区内指点交流。
先看下题主的问题:
// 一直搞不明白这个API的设计,这里要多一层current是什么用意,
// 难道除了current外还有别的用法吗?
// 如果只有获取值的用法,为什么不直接简化成myRef1.value拿呢?
const Test: React.FC = () => {
const myRef1 = React.useRef();
const onClick = React.useCallback(() => {
console.log('myRef1', myRef1.current.value)
}, [])
return <>
<input ref={myRef1} type="text" placeholder='点击提示createRef'/>
<button onClick={onClick}>点击获取ref</button>
</>
}
2
3
4
5
6
7
8
9
10
11
12
13
题主的核心问题是为什么要多一层current?把current去掉,直接用myRef1.value不行吗?要想聊明白这个问题,就得聊聊JavaScript中的基本数据类型和引用数据类型。
# 基本数据类型
我们先定义一个变量,叫baseType
let baseType = 10
后续的操作中,改变了变量baseType中的数据
baseType = 20
那么这时候,变量baseType的内存指向是谁?指向的是内存中有20这个值的具体地址。如果不太理解的,我们可以根据JavaScript的执行步骤走一下:
基本数据类型的内存指向
- 当js执行到
baseType = 10这行逻辑的时候,会在内存中开辟一个空间,空间中存储的值是10 - 这时候将内存空间的地址赋值给
baseType变量 - 当前JS继续执行到
baseType = 20这行逻辑的时候,也会在内存中开辟一个空间,空间中存储的值是20 - 然后再将20这个内存空间的地址赋值给
baseType变量
上述JavaScript的执行步骤可以理解吗?暂时还没有理解的没关系,给大佬上动图:
# 引用数据类型
如果是引用数据类型呢?引用数据类型的地址是如何指向的?
首先,我们先定义一个变量,名称叫做referencedType,然后给它加一个字段
let referencedType = { }
referencedType.a = '我是老A'
2
后续操作中改变了变量referencedType中字段a的数据:
referencedType.a = '我发生了变异'
执行完上述操作,并没有改变referencedType的内存指向。那么是改变了谁的内存指向呢?改变的是referencedType中字段a的内存指向。
所以,上述操作中,对于js来讲它的执行步骤是什么样子的?
引用数据类型中字段值的改变
- 当js执行到
referencedType = { }这行逻辑的时候,会在内存中开辟一个空间,空间中存储的值是一个空对象{} - 这时候将内存空间的地址赋值给
referencedType变量 - 当前JS继续执行到
referencedType.a = '我是老A'这行逻辑的时候,js发生了如下的操作- 首先会开辟一个内存空间,空间中存储的值是
我是老A - 然后在
referencedType变量上新增一个字段a并将我是老A这个内存地址赋值给字段a - 这时候
referencedType变量的具体值是:{a: '我是老A'}
- 首先会开辟一个内存空间,空间中存储的值是
- 当前JS继续执行到
referencedType.a = '我发生了变异'这行逻辑的时候,js发生了如下的操作- 首先会开辟一个内存空间,空间中存储的值是
我发生了变异 - 然后将
我发生了变异这个内存地址重新赋值给referencedType变量上的字段a - 这时候
referencedType变量的具体值是:{a: '我发生了变异'}
- 首先会开辟一个内存空间,空间中存储的值是
上述JavaScript的执行步骤可以理解吗?暂时还没有理解的没关系,给大佬上动图:
这是引用数据类型中字段值的改变,如果我想直接改变referencedType这个引用数据类型的内存指向呢?
其实很简单,直接给变量referencedType重新赋值一个新的数据(可以是任意数据类型)。看代码:
// 重新赋值一个新的对象
referencedType = { six: '我是老6' }
// 重新赋值一个字符串给变量 referencedType
referencedType = '我是变量referencedType'
2
3
4
那就有人说了,如果我不想修改变量referencedType的内存地址,也不想让其他人修改。这时候应该怎么办呢?
其实ES6已经给出了答案:const。使用const声明一个常量,如果后续在代码中直接给referencedType赋值,是会抛错的。看代码:
const referencedType = { a: '我是老A' }
// 后续代码中想要修改referencedType的引用地址
referencedType = { six: '我是老6' } // js执行到这一步的时候将抛错。
2
3
如果有大佬想知道const、let 、var 这三个关键字声明变量的区别可以移步看下这篇文章:从三个for循环来理解js变量声明 (opens new window)
# createRef为什么需要多一层current
如果大佬们有理解上文所说的内容,那么看到这里其实就不用看了,所有的答案都在上文中。当然,在大佬们离开前卑微求一个赞
OK,回归正题。
# 场景复现
当我们给一个DOM节点绑定ref的时候,该语法是这样的:
function TestRef() {
const ref = React.useRef()
return (
<div ref={ref}>
// 其他业务逻辑...
</div>
)
}
export default TestRef
2
3
4
5
6
7
8
9
那么在react中,ref的绑定大致可以这样描述(只是伪代码,不可较真):
const ref = {current: void 0}
function TestRef() {
ref.current = new HTMLDivElement()
}
2
3
4
如果TestRef这个组件内部有useState的操作,那么就会触发render函数的重新执行。可以这样理解:
// 触发了一次 useState操作,执行一次TesetRef方法
TestRef()
// 又触发了一次 useState操作,又执行了一次TestRef方法
TestRef()
2
3
4
根据上面的场景复现,我们可以这样理解:
每当render重新渲染,我们拿到的其实都是一个新的DOM对象,那么也就是一个新的内存地址。这就是为什么会多一层current的原因。如果没有这一层current,那么在render更新的时候就会导致ref绑定的丢失。
# 结尾及勘误和说明
以上就是这篇文章的全部了,如果文章的内容有帮助到你,欢迎点赞、评论。如果有大佬发现文章内的一些错误,恳请大佬多多指点。在此多谢大佬🙇♀️。
- 在社区内发现的这个问答在这里:飞机票✈️ (opens new window)
- 文章首发于个人博客:飞机票✈️ (opens new window)