前言
我们知道react的Hook为函数式组件创造了useState这个API,它主要是返回一个初始化数据以及修改这个初始化数据的函数来替代class组件中的setState和this.state,今天我们就尝试手动模拟实现useState的功能
Base Code
import React from "react";
import ReactDOM from "react-dom";
//------分割线-------
const rootElement = document.getElementById("root");
const App = () => {
const [n, setN] = React.useState(0);
const add = () => {
setN(n + 1);
};
return <button onClick={add}>此时的n为:{n}</button>;
};
//------分割线-------
ReactDOM.render(<App />, rootElement);
上面的代码我们使用了正常的useState方法来实现点击button后n+1操作,显而易见,当setN后,传入的n+1应当会替换掉原来的n而不是在n的基础上+1,这也符合react的设计思想,它并不希望我们创建某个值后再对他进行修改,反而更希望创建新的数据来替换掉它们。
下面手动实现一下
const myUseState =(initValue)=>{
console.log(initValue)//看这行代码
const setState=(newValue)=>{
initValue=newValue
console.log(initValue)//看这行代码
ReactDOM.render(<App />, rootElement);
}
return [initValue,setState]
}
上面的代码可以初始化数据并且返回了修改这个数据的函数,在函数执行后,还更新了视图。
但是在点击后,发现页面上的n并没有改变,依然是0
发生了什么?
查看上面两行log,可以发现打印出了0 1 0,说明页面执行render后,n依然为0,并没有改变。
我们用顺序来描述一下数据跟render的影响过程
useState(0)并return虚拟dom-->render渲染画面,插入虚拟dom
-->点击button,setN(n+1)-->render,读取虚拟dom的内容
-->执行App函数-->useState(0)并return虚拟dom(问题所在)
说明每次render的时候,重新执行了App函数,又初始化了一遍数据
解决
let state;//设置第三方变量,此时为undefined
const myUseState = (initValue) => {
//第一次执行函数时,state为undefined
//第二次执行函数时,不再把initValue给它,就可以避免再次render时又初始化一遍
state = state === undefined ? initValue : state;
const setState = (newValue) => {
state = newValue;
ReactDOM.render(<App />, rootElement);
};
return [state, setState];
};
貌似可以解决问题,此时点击+1功能正常
但如果此时,我增加state数据,假如为m,怎么办?
Base Code2
const App = () => {
const [n, setN] = myUseState(0);
const [m, setM] = myUseState(0);
const addN = () => {
setN(n + 1);
};
const addM = () => {
setM(m + 1);
};
return (
<>
<button onClick={addN}>此时的n为:{n}</button>
<hr />
<button onClick={addM}>此时的m为:{m}</button>
</>
);
};
myUseState可以修改为
let state = [];
let index = 0;
const myUseState = (initValue) => {
state[index] = state[index] === undefined ? initValue : state[index];
console.log(state) //log
const setState = (newValue) => {
state[index] = newValue;
console.log(state)//log
ReactDOM.render(<App />, rootElement);
};
index += 1;
return [state[index - 1], setState];
};
上面的代码依然是有问题的,通过两行log查看问题,可以发现index是一直增加的。
那么我们可以设置一个第三方变量,并且每次render后,把index清除。
let state = [];
let index = 0;
const myUseState = (initValue) => {
const currentIndex = index;//记录index
state[currentIndex] =
state[currentIndex] === undefined ? initValue : state[currentIndex];
console.log(state);
const setState = (newValue) => {
console.log(state);
state[currentIndex] = newValue;
render();
};
index += 1;
return [state[currentIndex], setState];
};
const render = () => {
index = 0;//清除index
ReactDOM.render(<App />, rootElement);
};
上面设置了第三方变量currentIndex来存放index,假设有多个初始化数据,那就执行多次myUseState函数并把初始值放入state数组中,当点击后渲染时,则把index重新设置为0。
useState规则
- index由数组的顺序决定,每次执行函数组件,都会读取数组的内容,如果数组的顺序不对,那么就会报错
- 当setState时,就会再次执行render函数,并且执行组件函数,然后通过react的diff算法分析两者区别,如果发现有不同,就会将不同的部分推送给虚拟DOM树,再执行页面更新
存在多个n
React的原理并不是直接对数据进行修改,而是对数据进行替换从而修改画面,所以从内存中看,实际上存在多个n,分别是旧值和新值。
当每次更新n后,都会触发render,然后重新执行一遍组件函数,把n从旧值设置为新的值。
不过要注意,setState函数触发的效果是异步的,比如说按照下面代码
function App() {
console.log("render");//每次render后都会执行这个
const [n, setN] = myUseState(0);
const add = () => {
for (let i = 0; i < 3; i++) {
setN(n + 1); //setN会触发render
console.log(n);//这是多少?
}
};
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={add}>+1</button>
</p>
</div>
);
}
当我点击后执行了三遍setN,按照理论上来说应该执行两次setN:第一次打印出“render”,并把n设置成1,再打印出1。第二次打印出render,并把n设置成2,并打印出2
但是实际结果却是0、0、0、render,并且n只加了1
说明setN是异步的。
所以上面打印出来的n都是旧值。如果我们不希望这样,那么可以把setState内传入一个函数
setN((x)=>{return x+1}) 这里的x指的是旧的n,react会自动识别这里的函数,把旧的n作为参数传入函数中,并且返回一个新的n,最后进行页面更新。
function App() {
console.log("render");
const [n, setN] = React.useState(0);//这里用回react.useState
const add = () => {
for (let i = 0; i < 3; i++) {
setN((n) => {
return n + 1;//n就是旧的值
});
}
};