十号

vuePress-theme-reco 十号    2022
十号

Choose mode

  • dark
  • auto
  • light
主页
分类
  • 《小狗钱钱》
  • 浏览器
  • Docker
  • note
  • 微前端
  • javascript
  • React
  • 工具
  • 工具函数
  • vue
TimeLine
简介

十号

20

Article

19

Tag

主页
分类
  • 《小狗钱钱》
  • 浏览器
  • Docker
  • note
  • 微前端
  • javascript
  • React
  • 工具
  • 工具函数
  • vue
TimeLine
简介

从setState更新机制来理解事件循环

vuePress-theme-reco 十号    2022

从setState更新机制来理解事件循环

十号 2022-08-10 react

前情提要

在逛技术社区的过程中遇到这么一个问题:为什么获取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>
  </>
}
1
2
3
4
5
6
7
8
9
10
11
12
13

题主的核心问题是为什么要多一层current?把current去掉,直接用myRef1.value不行吗?要想聊明白这个问题,就得聊聊JavaScript中的基本数据类型和引用数据类型。

# 基本数据类型

我们先定义一个变量,叫baseType

let baseType = 10
1

后续的操作中,改变了变量baseType中的数据

baseType = 20
1

那么这时候,变量baseType的内存指向是谁?指向的是内存中有20这个值的具体地址。如果不太理解的,我们可以根据JavaScript的执行步骤走一下:

基本数据类型的内存指向

  • 当js执行到baseType = 10这行逻辑的时候,会在内存中开辟一个空间,空间中存储的值是10
  • 这时候将内存空间的地址赋值给baseType变量
  • 当前JS继续执行到baseType = 20这行逻辑的时候,也会在内存中开辟一个空间,空间中存储的值是20
  • 然后再将20这个内存空间的地址赋值给baseType变量

上述JavaScript的执行步骤可以理解吗?暂时还没有理解的没关系,给大佬上动图:

# 引用数据类型

如果是引用数据类型呢?引用数据类型的地址是如何指向的?

首先,我们先定义一个变量,名称叫做referencedType,然后给它加一个字段

let referencedType = { }
referencedType.a = '我是老A'
1
2

后续操作中改变了变量referencedType中字段a的数据:

referencedType.a = '我发生了变异'
1

执行完上述操作,并没有改变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'
1
2
3
4

那就有人说了,如果我不想修改变量referencedType的内存地址,也不想让其他人修改。这时候应该怎么办呢?

其实ES6已经给出了答案:const。使用const声明一个常量,如果后续在代码中直接给referencedType赋值,是会抛错的。看代码:

const referencedType = { a: '我是老A' }
// 后续代码中想要修改referencedType的引用地址
referencedType = { six: '我是老6' } // js执行到这一步的时候将抛错。
1
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
1
2
3
4
5
6
7
8
9

那么在react中,ref的绑定大致可以这样描述(只是伪代码,不可较真):

const ref = {current: void 0}
function TestRef() {
  ref.current = new HTMLDivElement() 
}
1
2
3
4

如果TestRef这个组件内部有useState的操作,那么就会触发render函数的重新执行。可以这样理解:

// 触发了一次 useState操作,执行一次TesetRef方法
TestRef()
// 又触发了一次 useState操作,又执行了一次TestRef方法
TestRef()
1
2
3
4

根据上面的场景复现,我们可以这样理解:

每当render重新渲染,我们拿到的其实都是一个新的DOM对象,那么也就是一个新的内存地址。这就是为什么会多一层current的原因。如果没有这一层current,那么在render更新的时候就会导致ref绑定的丢失。

# 结尾及勘误和说明

以上就是这篇文章的全部了,如果文章的内容有帮助到你,欢迎点赞、评论。如果有大佬发现文章内的一些错误,恳请大佬多多指点。在此多谢大佬🙇‍♀️。

  • 在社区内发现的这个问答在这里:飞机票✈️ (opens new window)
  • 文章首发于个人博客:飞机票✈️ (opens new window)