React hooks的使用规则到底是怎么回事?

前言

在学习React的过程中,你可能会好奇React hooks为什么有这样的使用规则

  • 只能在组件顶层调用 Hooks,不要在循环、条件或嵌套函数内调用 Hooks。
  • 只能在 React 组件中调用 Hooks

希望后者对你来说是不言而喻的,毕竟React得用某种方式把hooks和组件联系起来

简短的答案是 React 依赖于 Hooks 的调用顺序,在这篇短文中,我们会尝试理解React Hooks

提醒

最近我读了篇文章React hooks: not magic, just arrays和看了 Deconstructing React || Tejas Kumar 解构React的演讲.这篇文章是在此基础上写的,主要是帮助自己理解所学的东西。如果你发现一些错误或者对此有更好的理解,请随时在评论区指出。

useState hooks是如何运作的?

状态管理其实是关于数组(state management is all about array),来看个例子吧

js 复制代码
// Note:This is just a mental model to help you understand how it works 
// but NOT necessarily how the API works internally.

const states = [] //initially the component does not have a state
let stateCursor = 0 // keep track of the current index in the `states` array.

function useState(initialStateOrUpdaterFunction) {
    const FRONZENCURSOR = stateCursor
    //initializes the state value once and set the same state value for subsequent calls.
    states[FRONZENCURSOR] = states[FRONZENCURSOR] || initialStateOrUpdaterFunction
    function setState(newState) {
        states[FRONZENCURSOR] = newState
        reRender() //function to re-render the component
    }
    stateCursor++
    return [states[FRONZENCURSOR], setState]
}

function App() {
    const [name,setName] = useState('mario')
    const [count,setCount] = useState(0)
    const [show,setShow] = useState(false)
    
    /*other code*/
}

注意

定义state的时候和stateCursor的时候,是在组件外定义的,这是因为

state作为组件的内存,状态不像常规变量那样在函数返回后消失。 state实际上"存在"在 React 本身中------就像在书架子上一样!(当组件需要的时候,React就在书架上拿给组件)------在你的函数之外

下面模拟一下整个执行过程

1. Initial Render

当每个useState()被调用时,各自setter函数将被绑定到相应的stateCursor。

举个例子,当执行const [name,setName] = useState('mario')时,setName函数将绑定到'cursor 0',也就是states[0]=newName 来更新状态。

当执行const [count,setCount] = useState(0)时,setCount函数将绑定到'cursor 1',并使用states[1]=newName来更新状态,依此类推 。

初始渲染后组件的状态从 [ ] 变成 ['mario',0,false]

2. Subsequent render(re-render)

当re-render)时,"stateCursor"将重置为"0",并且"states"会更新为新状态(记住:当组件的state或收到的props改变时,组件会re-render)

来看个正常调用的例子

js 复制代码
function App() {
    const [name,setName] = useState('mario')
    const [count,setCount] = useState(0)
    const [show,setShow] = useState(false)
    
    return <>
         <button onclick={() => setCount(count + 1)}>+</button>
         <span>count:{count}</span>
    </>
}

当我点击'+'按钮,() => setCount(count + 1)被调用,看initial Render图,还记得setCount(count + 1)函数对应什么吗?是states[1] = newCount,也就是states[1] = 0+1,组件的states被更新为['mario',1,false],然后触发re-render(记住,组件的状态更新时会触发re-render),re-render过程看下图

可以看到对应的setter函数被重新正确地绑定到对应的stateCursor,这是因为重新渲染的过程中,三个useState() hook 的调用顺序并没有发生变化,也没有哪个消失不见了,与上一次渲染完全一致。但如果我不按套路出牌......就像这样子

js 复制代码
let isCalled = false
function App() {
    let count
    let setCount
    const [name, setName] = useState('mario')
    if(!isCalled) {
        isCalled = true
        [count, setCount] = useState(0)
    }
    const [show, setShow] = useState(false)
    return <>
        <button onclick={() => setCount(count + 1)}>+</button>
        <span>count:{count}</span>
    </>
}

还是来模拟整个过程 initial render没有什么问题,跟正常调用的情况一样。关键是re-render,假设还是通过击'+'按钮触发re-render,这时候组件的状态为['mario',1,false],re-render过程:

  1. 执行const [name, setName] = useState('mario'),即执行name = states[0];setName = (newValue) => states[0] = newValue,第一个hook调用跟initial render一致,没有问题

  2. 不执行[count, setCount] = useState(0)因为isCalled = true

  3. 执行const [show, setShow] = useState(false),即执行show = states[1];setter = (newValue) => states[1] = newValue

可以看到show的值变成了1而非false,对应的setter函数也变成了改变count的值的函数,setter函数与对应状态的值不再像上一次渲染那样保持一致。如果你在条件里调用hook,React就像是:天知道你这个条件下次是真是假,因此React无法保证与上次一致的调用顺序,这种行为会导致各种各样无法预测的问题

如果在loop里调用会发生什么呢

js 复制代码
function App() {
    let value,setValue
    
    const [name,setName] = useState('mario')
    const [count,setCount] = useState(0)
    const [show,setShow] = useState(false)
    
    for (let i = 0; i < 5; i++) {
        // Attempting to call useState inside a loop
        [value, setValue] = useState(i);
    }
    /*other code*/
}

读者可以试着模拟一下,最终的结果是

states=['mario',0,false,0,1,2,3,4]

value=4

setValue=(newValue) => states[7] = newValue

虽然状态被成功初始化了,但value和setValue却只能对应最后一个状态初始化,也就是说之前的初始化毫无意义,我们既不能读取也不能修改。

写在最后的话

差不多就是这样了,如果你感兴趣可以试着模拟在嵌套函数调用hooks的情况

相关推荐
阿伟来咯~2 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
bysking3 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
September_ning8 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人8 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0018 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
Rattenking10 小时前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
熊的猫11 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
小牛itbull15 小时前
ReactPress:重塑内容管理的未来
react.js·github·reactpress