useState 最初版
如果只是想实现react里面的计数器演示demo,很简单,下面的代码可以直接满足。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>useState age</title>
</head>
<body>
<div id="root"></div>
<script>
let memoizedState;
function useState(initialState) {
if (memoizedState === undefined) {
memoizedState = initialState;
}
const setState = (action) => {
memoizedState = typeof action === "function" ? action(memoizedState) : action;
render()
}
return [memoizedState, setState]
}
function App() {
const [age, setAge] = useState(0);
window.setAge = setAge;
return `
<button onclick=" window.setAge((prev) => {
return prev + 1
});">${age}</button>
`
}
const render = () => {
document.getElementById('root').innerHTML = App();
}
render();
</script>
</body>
</html>现有的实现里面只有 age 状态,看起来没问题
如果我想给他加上name,点击name对应的容器 让Name变成Tom。 那么我是不是可以这么做,通过判断不同的初始值的类型 去决定不同状态的更新。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script>
let numberMemoizedState;
let stringMemoizedState;
function useState(initialState) {
const isNumber = typeof initialState ==="number"
if (isNumber) {
if (numberMemoizedState === undefined) {
numberMemoizedState= initialState;
}
} else {
if (stringMemoizedState === undefined) {
stringMemoizedState= initialState;
}
}
const setState = (action) => {
if (isNumber) {
numberMemoizedState = typeof action === "function" ? action(numberMemoizedState) : action;
} else {
stringMemoizedState = typeof action === "function" ? action(stringMemoizedState) : action;
}
render();
}
return [isNumber ? numberMemoizedState : stringMemoizedState, setState]
}
function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState('jack');
window.setCount = setCount;
window.setName = setName;
return
<div onclick="window.setName('tom')">
name ${name}
</div>
<button onclick=" window.setCount((prev) => {
return prev + 1
});">${count}</button>
}
const render = () => {
document.getElementById('root').innerHTML = App();
}
render();
</script>
</body>
</html>如果只是实现我上面的两种需求,这个useState我觉得已经满足要求了,但是 如果还要加上address字段呢? 现有的用类型判断的方法是不是失效了?
所以可以换一种思路:不用类型区分状态,而是用数组保存所有状态,再用下标记录每一次 useState 对应的位置。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>useState hooks</title>
</head>
<body>
<div id="root"></div>
<script>
let hooks = [];
let hookIndex = 0;
function useState(initialState) {
let currentIndex = hookIndex;
if (hooks[currentIndex] === undefined) {
hooks[currentIndex] = initialState;
}
const setState = (action) => {
hooks[currentIndex] = typeof action === "function" ? action(hooks[currentIndex]) : action;
render();
}
hookIndex ++;
return [hooks[currentIndex], setState]
}
function App() {
const [age, setAge] = useState(18);
const [name, setName] = useState('Jack');
const [address, setAddress] = useState('Github Repo');
const [techStack, setTechStack] = useState('JavaScript');
window.setAge = setAge;
window.setName = setName;
window.setAddress = setAddress;
window.setTechStack = setTechStack;
return `
<div onclick = "window.setName('Tom')" >
name: ${name}
</div>
<button onclick="window.setAge((prev) => {
return prev + 1
});">age: ${age}</button>
<div onclick = "window.setAddress('Gitlab Repo')" >
address: ${address}
</div>
<div onclick = "window.setTechStack('Tech Stack')" >
techStack: ${techStack}
</div>
`
}
const render = () => {
hookIndex = 0;
document.getElementById('root').innerHTML = App();
}
render();
</script>
</body>
</html>上面这个实现,已经可以满足我的需求了,那为啥 React 用的不是数组 + 下标,而是链表?这个到底有啥好处。 先不着急,我发现了一个问题。我上面这个写法,每次setState的时候就会render一次。我先来解决这个问题。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mini useState</title>
</head>
<body>
<div id="root"></div>
<script>
let hooks = [];
let hookIndex = 0;
let renderCount = 0;
let isBatching = false;
function batch(fn) {
isBatching = true;
fn();
isBatching = false;
render();
}
function useState(initialState) {
const currentIndex = hookIndex;
if (hooks[currentIndex] === undefined) {
hooks[currentIndex] = {
memoizedState:
typeof initialState === 'function' ? initialState() : initialState,
queue: [],
};
}
const hook = hooks[currentIndex];
if (hook.queue.length > 0) {
let newState = hook.memoizedState;
for (const action of hook.queue) {
newState =
typeof action === 'function'
? action(newState)
: action;
}
hook.memoizedState = newState;
hook.queue = [];
}
function setState(action) {
hook.queue.push(action);
if (!isBatching) {
render();
}
}
hookIndex++;
return [hook.memoizedState, setState];
}
function App() {
const [age, setAge] = useState(18);
const [name, setName] = useState('Jack');
const [address, setAddress] = useState('Github Repo');
const [techStack, setTechStack] = useState('JavaScript');
function handleAgeClick() {
setAge((prev) => prev + 1);
setAge((prev) => prev + 1);
setAge((prev) => prev + 1);
}
function handleNameClick() {
setName('Tom');
}
function handleAddressClick() {
setAddress('Gitlab Repo');
}
function handleTechStackClick() {
setTechStack((prev) =>
prev === 'JavaScript' ? 'React' : 'JavaScript'
);
}
window.handleAgeClick = handleAgeClick;
window.handleNameClick = handleNameClick;
window.handleAddressClick = handleAddressClick;
window.handleTechStackClick = handleTechStackClick;
window.batch = batch;
console.log('hooks:', hooks);
return `
<div>
<button onclick="batch(window.handleAgeClick)">
age: ${age}
</button>
<div onclick="batch(window.handleNameClick)">
name: ${name}
</div>
<div onclick="batch(window.handleAddressClick)">
address: ${address}
</div>
<div onclick="batch(window.handleTechStackClick)">
techStack: ${techStack}
</div>
</div>
`;
}
function render() {
hookIndex = 0;
renderCount++;
console.log('渲染了', renderCount, '次');
document.getElementById('root').innerHTML = App();
}
render();
</script>
</body>
</html>下来在看下为什么我们一直以来的实践都是把useState的hook放在最上面 且不要在if里面写setState
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mini useState</title>
</head>
<body>
<div id="root"></div>
<script>
let hooks = [];
let hookIndex = 0;
let renderCount = 0;
let isBatching = false;
function batch(fn) {
isBatching = true;
fn();
isBatching = false;
render();
}
function useState(initialState) {
const currentIndex = hookIndex;
if (hooks[currentIndex] === undefined) {
hooks[currentIndex] = {
memoizedState:
typeof initialState === 'function' ? initialState() : initialState,
queue: [],
};
}
const hook = hooks[currentIndex];
if (hook.queue.length > 0) {
let newState = hook.memoizedState;
for (const action of hook.queue) {
newState =
typeof action === 'function'
? action(newState)
: action;
}
hook.memoizedState = newState;
hook.queue = [];
}
function setState(action) {
hook.queue.push(action);
if (!isBatching) {
render();
}
}
hookIndex++;
return [hook.memoizedState, setState];
}
function App() {
const [age, setAge] = useState(18);
const [name, setName] = useState('Jack');
if (age > 20) {
const [flag] = useState(null);
console.log('flag', flag);
}
const [address, setAddress] = useState('Github Repo');
const [techStack, setTechStack] = useState('JavaScript');
function handleAgeClick() {
setAge((prev) => prev + 1);
setAge((prev) => prev + 1);
setAge((prev) => prev + 1);
}
function handleNameClick() {
setName('Tom');
}
function handleAddressClick() {
setAddress('Gitlab Repo');
}
function handleTechStackClick() {
setTechStack((prev) =>
prev === 'JavaScript' ? 'React' : 'JavaScript'
);
}
window.handleAgeClick = handleAgeClick;
window.handleNameClick = handleNameClick;
window.handleAddressClick = handleAddressClick;
window.handleTechStackClick = handleTechStackClick;
window.batch = batch;
console.log('hooks:', hooks);
return `
<div>
<button onclick="batch(window.handleAgeClick)">
age: ${age}
</button>
<div onclick="batch(window.handleNameClick)">
name: ${name}
</div>
<div onclick="batch(window.handleAddressClick)">
address: ${address}
</div>
<div onclick="batch(window.handleTechStackClick)">
techStack: ${techStack}
</div>
</div>
`;
}
function render() {
hookIndex = 0;
renderCount++;
console.log('渲染了', renderCount, '次');
document.getElementById('root').innerHTML = App();
}
render();
</script>
</body>
</html>当点击age的时候 会执行 click事件函数,函数里面会setState吧state改成21。这个时候重新触发渲染,所有的hook都重新执行,那么之前在hooks数组 里面根据下标保存的memoizedState是不是就错乱了。看到控制台的输出 flag: Github Repo