React文档深扒:useState背后的真相!

226 阅读4分钟

前言

众所周知, 在react中,如果想保存状态数据,那么就要在组件中使用useState 钩子函数来实现。不知道你有没有思考过下面的问题。

为什么useState知道如何返回正确顺序的值?

useState又是如何缓存正确数据的呢?

React的官方文档中找到了相关解释的实例。

useState

首先来回顾一下,为什么存在useState这个hook?

为什么需要useState?

当我们在声明一个函数式的组件时,可以在里面声明一个局部变量。局部变量具有以下特性:

  1. 局部变量是无法在多次渲染中持久保存的, 因为每次重新渲染整个组件时,React都会从头开始渲染,不会保留之前对局部变量的任何更改。
  2. 更改局部变量不会触发渲染。 因为上面的两点原因, 所以下面这段代码最后实现的点击后没有任何效果
function App() {
  let index = 0

  function onAdd () {
    index = index + 1
  }
  return (
      <div>
        index的值: {index}
        <button onClick={onAdd}>点击增加index</button>
      </div>
  )
}

image.png

状态变量

当我们调用useState, 就是在告诉React我们希望这个组件能够在下一次渲染时,记住这个状态,并且useState返回的修改方法会额外触发渲染的逻辑。如此也就是 set函数就会更新下一次渲染的状态变量。 例如下面这段代码,就会实现点击+1的效果

function App() {
  const [index, setIndex] = useState<number>(0)
  function onAdd () {
    setIndex(index + 1)
  }
  return (
      <div className="app">
        index的值: {index}
        <button onClick={onAdd}>点击增加index</button>
      </div>
  )
}

image.png

React 如何知道返回哪个state?

相信大家都已经发现了,useState在调用时,没有给他任何显式的特殊标注信息,也就是没有对应的映射关系,那么在有多个状态的情况下react是如何是如何知道自己setState, set的是哪一个状态, 返回的又是哪一个state变量呢?

其实react在设计时,为了使语法更加简洁,为每一个组件保存了一个数组,其中数组中每一项都是一个state对。它维护了当前state对的索引值。 然后每次渲染之前都将其设置为0, React会从索引值0开始,遍历保存状态数据的数组,依次进行读取, 如果当前已存在这个state对,即证明state对存在,返回存下的state对即可。

代码实现

下面我们就来参考react官网给出的简版示例,一步一步的实现useState的特性。

useState

在下面的代码中, 实现了一个简版的useState, 其本质,就是不会被刷新的数组存储每一次的useState的调用,然后在被setState更新dom时,重置currentHookIndex索引,然后再从头依次读取componentHooks下的缓存。

  • 如果存在缓存,就返回上一次更新的内容(setState会通过闭包更新state)。

  • 如果不存在缓存, 就将initialState和更新方法存入componentHooks的缓存中。

let componentHooks = []
let currentHookIndex = 0;

function useState (initialState) {
    let pair = componentHooks[currentHookIndex];

    // 如果pair存在,说明上次渲染时,已经存在,直接返回pair即可。
    if (pair) {
        currentHookIndex ++;
        return pair
    }
    pair = [initialState, setState]
    // 初次渲染
    function setState (nextState) {
        pair[0] = nextState;
        // 更新DOM
        updateDOM() // 暂未实现
    }
    
    // 更新componentsHooks 列表
    componentHooks[currentHookIndex] = pair;
    // 更新currentHookIndex, 然后为下一次调用做准备
    currentHookIndex ++;
    
    return pair
}


function updateDOM () {
    // 将“函数式组件”返回值,结合在DOM上。
}

APP

可以简单认为是上文中的函数式组件, 只是这里的返回值不再是jsx语法, 而是返回一个对象,将存储的状态,和更新状态的方法传出去。

  • 调用useState。
  • 声明改变状态的函数。
function App () {
    const [index, setIndex] = useState(0);
    function onAdd () {
        setIndex(index + 1)
        console.log(index)
    }

    return {
        index,
        onAdd
    }
}

updateDOM

这个函数主要实现的是代替原本react的工作, 将状态和更新状态的函数与DOM进行结合。

  • 获取DOM。
  • 获取APP返回的状态,和更新状态的函数。
  • 赋值并更新DOM。
function updateDOM () {
    currentHookIndex = 0
    // 在渲染组件之前, 重制hook下标
    let btn = document.querySelector('.add-btn')
    let numberDom = document.querySelector('.number')
    const output = App()
    numberDom.innerHTML = output.index
    btn.onclick = output.onAdd
}

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>easy-state</title>
</head>
<style>

</style>
<body>
    <div class="app">
        <div class="number"></div>
        <div class="add-btn">点击加1</div>
    </div>
</body>
<script src="index.js"></script>
</html>

index.js完整代码

let componentsHooks = []
let currentHookIndex = 0;

function useState (initialState) {
    let pair = componentsHooks[currentHookIndex];

    // 如果pair存在,说明上次渲染时,已经存在,直接返回pair即可。
    if (pair) {
        currentHookIndex ++;
        return pair
    }
    pair = [initialState, setState]
    // 初次渲染
    function setState (nextState) {
        pair[0] = nextState;
        // 更新DOM
        updateDOM() // 暂未实现
    }

    // 更新componentsHooks 列表
    componentsHooks[currentHookIndex] = pair;
    // 更新currentHookIndex, 然后为下一次调用做准备
    currentHookIndex ++;

    return pair
}

function App () {
    const [index, setIndex] = useState(0);
    function onAdd () {
        setIndex(index + 1)
        console.log(index)
    }

    return {
        index,
        onAdd
    }
}


function updateDOM () {
    currentHookIndex = 0
    // 在渲染组件之前, 重制hook下标
    let btn = document.querySelector('.add-btn')
    let numberDom = document.querySelector('.number')
    const output = App()
    numberDom.innerHTML = output.index
    btn.onclick = output.onAdd
}
updateDOM()

最终效果如下图,点击+1:

image.png

结尾

通过数组存储实现的方式,隐时的声明了state和hooks之间的映射关系,也因此如果hooks的调用在条件语句、循环语句或其他嵌套函数内的情况下, 就会导致无法正确的识别执行顺序。(如果文章中有错误内容,欢迎大家交流指正。)

相关链接

从0到1搭建react组件库-项目规范篇

从0到1搭建react组件库-开发方案篇

从0到1搭建react组件库-文档篇