写这边文章主要是为了解决自己心中对useState用法的疑惑,希望能够帮助大家。
useState的用法其实非常简单,看官方文档非常容易掌握它的写法。但用了一段时间后,会有一些疑惑,比如:每次调用useState返回的 setXXX 方法以后触发函数重新执行,得到的新状态为什么能保留下来(明明调用 useState 传递的参数还是跟之前一样)。useState为什么不能放到 if 分支里?所以这遍文章主要回答下这2个问题。
为什么每次渲染可以保留最新的状态
可以根据自己以往的编程经验猜测出每次调用setXXX后 "状态" 肯定是保存到了某个地方,再次渲染的时候从那个地方把值拿出来,所以很容易写出了下面的代码:
// 触发页面重新渲染
function reRender() {
ReactDOM.render(<App />, document.getElementById("root"));
}
// 内部变量保存更新后的数据
let _innerState = undefined;
// 更新状态,重新渲染组件
function _setInnerState(newState) {
_innerState = newState;
reRender();
}
function useState(initValue) {
// 判断是不是第一次调用useState
if (_innerState == null) {
_innerState = initValue;
}
return [_innerState, _setInnerState];
}
function Counter() {
const [count, setCount] = useState(0);
return (
<div className="comp">
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>点我+1</button>
</div>
);
}
export default function App() {
return (
<div className="App">
<Counter />
</div>
);
}
上面的useState是自定义的函数,而非react hooks的useState。实现也很简单,为了保证每次执行Counter函数都能获取到最新的值,用变量 _innerState 来保存每次setCount后的值。第一次调用useState的时候 _innerState为空,所以会把传递的0赋值给 _innerState。点击按钮 值加1后,会把1复制赋值给 _innerState,这个时候组件Counter再次渲染,函数Counter重新执行,虽然useState(0)还是传递了参数0,但是由于已经不是第一次调用useState,所以传给useState的参数0被忽略,直接使用内部变量 _innerState的值(此时 _innerState 已经是1),所以渲染到页面的数据就是1而不是0
为什么useState不能放到if里面
上面的代码显然还有问题,如果在Counter函数里多次调用useState是不行的,因为只用 _innerState 保留了一个状态,如果在Counter函数中定义了多个状态(多次使用useState)会有问题,那么怎么办呢?很容易想到,用数组这个数据结构来存储状态。
// 触发页面重新渲染
function reRender() {
ReactDOM.render(<App />, document.getElementById("root"));
}
// 用数组来存储
let _innerStateArr = [];
let _index = 0;
function _setInnerState(index, newValue) {
_innerStateArr[index] = newValue;
reRender();
// 需要重置
_index = 0;
}
const _updateFn = index => newValue => {
_setInnerState(index, newValue);
};
function useState(initValue) {
if (_innerStateArr[_index] == null) {
_innerStateArr[_index] = initValue;
}
const cur = _innerStateArr[_index];
return [cur, _updateFn(_index++)];
}
function Counter() {
// 这里用useState定义了2个状态
const [name, setName] = useState("james");
const [count, setCount] = useState(0);
function updateData() {
setName("kobe.r.i.p" + (count + 1));
setCount(count + 1);
}
return (
<div className="comp">
<p>{name}</p>
<p>{count}</p>
<button onClick={() => updateData()}>点我+1</button>
</div>
);
}
export default function App() {
return (
<div className="App">
<Counter />
</div>
);
}
上面的代码阅读起来应该也没有难度。用_innerStateArr来存储每个状态。Counter函数中,useState("james")是第一次调用useState,所以把james的值存到了数组的第一个位置,即 _innerStateArr[0]='james'。useState(0)是第二次调用useState,所以把0的值存到了数组的第二个位置,即 _innerState[1]=0。所以就实现了在函数中多次调用useState的需求。
理解了上面的代码,就可以解释为什么useState不能放到 if 中的问题了。useState是根据你调用的顺序来决定是到底是使用哪个状态,如果你把useState放到了 if 中,顺序就被打乱,那么用useState得到的状态很可能是错误的,看这段代码:
function Counter() {
const [name, setName] = useState("james");
if (name === 'james'){
// eslint-disable-next-line
const [other] = useState('other')
}
const [count, setCount] = useState(0);
function updateData() {
setName("kobe.r.i.p" + (count + 1));
setCount(count + 1);
}
return (
<div className="comp">
<p>{name}</p>
<p>{count}</p>
<button onClick={() => updateData()}>点我+1</button>
</div>
);
}
Counter函数代码基本没变,就是在 if 里增加了一个useState的调用。Counter被第一次调用时,if 分支满足条件,所以这个时候useState在Counter中调用了3次,它们的值被被赋予数组_innerStateArr对应的位置,此时count的值0被存到了数组 _innerStateArr第三个位置。而当点击按钮,Counter再次执行时,if 分支不满足条件,整个函数Counter里useState被调用了2次,useState(0)这个代码在Counter函数中是第二次被调用,既然是第二次被调用,根据调用顺序取到的就是数组 _innerStateArr第二个位置的值other,而不是1,这就是问题所在,所以不能在 if 使用useState。
总结
上面的代码解释清楚了useState的内部原理,用了2个内部变量_index和 _innerState,真实的React的useState肯定不是这么做的,它把 _innerState和 _index 放到了虚拟节点上存储,因为虚拟DOM和真实的DOM节点是一一对应的,每个函数组件又是一个虚拟DOM,这样每个组件内部就有它自己的 _innerState和 _index 了。
上面的代码只是帮助大家理解useState的工作流程,React Hooks的useState的实现代码肯定不是这样,但思路是一样的,希望对useState有同样疑惑的你有所帮助。