前言
在学习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过程:
-
执行
const [name, setName] = useState('mario')
,即执行name = states[0];setName = (newValue) => states[0] = newValue
,第一个hook调用跟initial render一致,没有问题 -
不执行
[count, setCount] = useState(0)
因为isCalled = true
-
执行
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的情况