【学习记录】 React Hooks 之 useState的工作原理

458 阅读4分钟

前言:我们在函数式组件中使用useState来为组件创建一个state,就能模拟类变量,这很神奇不是吗!用起来越方便,对它的了解就越模糊,本文是对useState的工作流程、数据结构的概括OwO

学习useState中产生的困惑

  • useState如何在重复渲染情况下,保存状态值
  • React如何管理多个state
  • React如何在每次重新渲染时返回最新的状态
  • 我们知道每个组件都可以使用useState,React又是如何管理不同组件的state

从问题出发

一.useState如何在重复渲染情况下,保存状态值

问题描述:在下面这个例子中,我们知道页面初次渲染,显示诸葛富贵,当我们点击按钮后,页面再次渲染,显示诸葛建国,那是不是说重渲染时const [name, setName] = useState('诸葛富贵')没有执行???

function App(){
  console.log('App render')
  const [name, setName] = useState('诸葛富贵')

  return (
    <div>
      <h1>name: {name}</h1>
      <button onClick={() => setName('诸葛建国')}>点我改名</button>
    </div> 
  )
}

答案:这行代码执行了,控制台打印了App render,但是没用到初始值诸葛富贵,而是返回修改后的值,也就是说初始值只在第一次渲染时有效。React在运行const [name, setName] = useState('诸葛富贵')这行代码时,useState内部 会判断此处是初始渲染还是重渲染如果是初始渲染,则返回初始值,如果是重渲染,计算出最新的state并返回。

示例: state: oldHook ? oldHook.state : initial,

2. React如何管理多个state

function App(){
  console.log('App render')
  const [name, setName] = useState('诸葛富贵')
  const [name1, setName1] = useState('上官翠花')
  const [name2, setName2] = useState('司马海味')

  return (
    <div>
      <h1>name: {name}</h1>
      <h1>name1: {name1}</h1>
      <h1>name2: {name2}</h1>
    </div> 
  )
}

管理Hook的是一个单向链表

1. 第一次渲染构建Hooks单向链表

image.png 第一次渲染App时,每运行一个useState(当然也可以是其他Hook,这里只用useState示例),都会向单向链表添加一个节点,这个节点称为hook,初始化state,然后返回[state, setState]

2. 重渲染阶段遍历Hooks单向链表

image.png 第一次渲染之后的称为重渲染,在重渲染阶段,我们就不需要再为添加节点,只需要从头节点遍历链表。

const [name, setName] = useState('诸葛富贵') 映射 Hooks[0] (链表的第一个节点),拿到state,并返回

const [name, setName] = useState('上官翠花') 映射 Hooks[1] (链表的第一个节点),拿到state,并返回

const [name, setName] = useState('司马海味') 映射 Hooks[2] (链表的第一个节点),拿到state,并返回

可以知道函数组件内的hook函数被React映射为一个链表,所以保持hook函数的执行顺序就是保持了正确的映射关系。

在React官方文档中有这么一段话: 只在最顶层使用 Hook 不要在循环,条件或嵌套函数中调用 Hook,  确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

官方文档的这段话告诉我们两个信息:

1.不能在while、if、function中使用Hook
2. Hook的调用顺序很重要

3.React如何在每次重新渲染时返回最新的状态

问题描述:对于同一个状态,我们可以多次调用setState去修改状态,我们称一个setState为一个action,但是setState是异步修改,那么React肯定要先保存所以的action,之后在某一时机去调用action,修改状态,返回最新的状态。

function App(){
  console.log('App render')  //输出两次
  const [name, setName] = useState('诸葛富贵')

  const change = (oldName)=>{
    setName('诸葛1')
    setName('诸葛2')
    setName('诸葛3')
    console.log(name) //'诸葛富贵'
  }

  return (
    <div>
      <h1>name: {name}</h1>
      <button onClick={change}>点我修改</button>
    </div> 
  )
}

点击按钮,console.log('App render')输出了几次?console.log(name)?

答:输出了两次,初始化渲染+一次重渲染

管理action的结构是一个循环链表

image.png 每调用一次setName,都会向hook里的queue添加一个节点,queue指向最后一个节点。

在update阶段,会从头节点开始遍历所有action,那么最后一次action就是最新的值。

这里有两点小疑问:

  1. 直接取最新的action的值不就可以了吗,为啥还要遍历之前的action。

答:因为action可能是一个函数,这需要之前action的状态作为参数。

  1. 既然遍历是从链表头开始的,那单链表也能实现,为啥要用循环链表? 答:如果使用单链表,想要找到最早的更新,就需要一层一层的找(这里我觉得保留头节点指针也可以鸭,为什么要一层一层的找呢),使用循环链表的话,queue.last.next就是最早的action

4.我们知道每个组件都可以使用useState,React又是如何管理不同组件的state

答:React用fiber节点存放hooks单向链表