浅析useState
本文简单实现了一个useState,并以此分析useState的特点。
useState干了什么?
前面已经初步学习了一些比较重要的hooks,其中就包括useState。
function App(){
const [n,setN] = React.useState(0)
return (
<div>
{n}
<button onClick={()=>{setN(n+1)}}>+1</button>
</div>
)
}
我们知道,useState可以返回读写2个与数据(state)有关的接口。useState接受一个具体的值或者具有返回值的函数作为参数吗,组件初次渲染的时候会将该参数作为state的初始值。同时当调用setState时,会触发组件的Re render,并且在后续的渲染中,useState返回state始终是更新后的state。
现在就让我们一步一步地实现一个简单的useState。
实现一个简单的useState
-
首先我们需要返回2个接口,并且能够初始化state的值。
function myUseState(initialValue){ let state = initialValue function setState(newValue){ state = newValue } return [state,setState] } function App() { const [n, setN] = myUseState(1); console.log( n ); return ( <div> {n} <button onClick={() => { setN(n + 1); }}>+1</button> </div> ); }
现在,我们获取到了一个能够初始化一个state的函数,但是这个函数也仅仅只能够初始化一个state。
-
实现setState的功能
实现setState的思路比较复杂,首先setState后,组件会触发再渲染,而再次渲染返回的state是更新之后的state。
let state; function myUseState(initialValue) { // 初次渲染和再次渲染返回的内容不同 state = state === undefined ? initialValue : state; function setState(newValue) { state = newValue; // setState后触发再渲染 render(); } return [state, setState]; } const rootElement = document.getElementById("root"); const render = () => { ReactDOM.render(<App />, rootElement); }; // App组件函数同上
这里我们把state的声明提到了 myUseState 函数的外面。如果state的声明在myUseState 函数里面的话,那么每次再渲染时都会新声明一个state覆盖掉setState后的state。
同时我们通过
state = state === undefined ? initialValue : state
进行判断组件是初次渲染还是再渲染,初次渲染时state的值为传入的值,再次渲染时state的值则是更新后的state。在setState中,我们新增了一个render函数,用于后续触发再渲染,更新页面UI。
-
解决组件中多个useState的问题
在2.中我们基本实现了简单useState的功能。那么如果一个组件中存在多个useState怎么办呢?
要用1个变量存储多组数据,很自然的就能想到使用对象
{}
或者是数组[]
。如果我们使用对象如:
state = {n:0,m:0}
,那么我们在执行useState(initialValue)时,怎么知道我们需要的是哪个变量呢?当然这并不排除有方法能使用变量来实现,但是很明显React并不是这么做的。React使用的是数组进行数据的存储。所以我们需要一个新的index变量来记录数据的位置。
let _state = []; let index = 0; function myUseState(initialValue) { // currentIndex记录当前index,否则最后return的时候会return _state[index+1] const currentIndex = index; _state[currentIndex] =_state[currentIndex] || initialValue; index += 1; function setState(newValue) { _state[currentIndex] = newValue; render(); } return [_state[currentIndex], setState]; } const rootElement = document.getElementById("root"); const render = () => { // 再渲染时,从头开始载入state数据 index = 0; ReactDOM.render(<App />, rootElement); };
根据上面的代码,我们不难发现,由于使用了数组进行存储数据,_state中存储的数据的顺序和组件中myUseState执行的顺序是一致的。这就很好地解释了为什么不能在 if-else 中使用useState,正是因为很有可能会打乱useState的执行顺序,导致数据存储位置的改变。
-
其他组件也需要使用state和index怎么办?
其他组件也有可能需要使用到useState,又或者说如果我们使用了2次同一个组件,那么声明在外面的state和index变量名不就冲突了吗?
因此我们需要把这两个变量名放在一个合适的地方,不会冲突的地方。
那么每个组件有没有一个这样的地方呢?有,React组件的虚拟节点。React就是这么做的。这样更新UI时,只需要用Diff算法比对虚拟节点的不同就能够找出组件更新的地方,以此作为UI更新的依据。
上面的过程,使用了简单的state和index作为简化,但是React实现的过程更为复杂:实际上React的节点是一个FiberNode(Fiber对象),而我们假设的state真实名称为 memorizedState,而index的实现并不是简单的依靠某个属性,而是通过链表进行实现的。
参考文章: