前言
众所周知, 在react中,如果想保存状态数据,那么就要在组件中使用useState 钩子函数来实现。不知道你有没有思考过下面的问题。
为什么useState知道如何返回正确顺序的值?
useState又是如何缓存正确数据的呢?
在React的官方文档中找到了相关解释的实例。
useState
首先来回顾一下,为什么存在useState这个hook?
为什么需要useState?
当我们在声明一个函数式的组件时,可以在里面声明一个局部变量。局部变量具有以下特性:
- 局部变量是无法在多次渲染中持久保存的, 因为每次重新渲染整个组件时,React都会从头开始渲染,不会保留之前对局部变量的任何更改。
- 更改局部变量不会触发渲染。 因为上面的两点原因, 所以下面这段代码最后实现的点击后没有任何效果
function App() {
let index = 0
function onAdd () {
index = index + 1
}
return (
<div>
index的值: {index}
<button onClick={onAdd}>点击增加index</button>
</div>
)
}
状态变量
当我们调用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>
)
}
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:
结尾
通过数组存储实现的方式,隐时的声明了state和hooks之间的映射关系,也因此如果hooks的调用在条件语句、循环语句或其他嵌套函数内的情况下, 就会导致无法正确的识别执行顺序。(如果文章中有错误内容,欢迎大家交流指正。)