手写 useState
代码实现
// 手写 useState
// 核心:闭包 + 数组 + 索引 cursor
function createRenderer(Component) {
const states = [];
let cursor = 0; //当前执行到了第几个 useState
function useState(initialValue) {
const currentIndex = cursor;
//只有第一次渲染的时候才使用初始值
if (states[currentIndex] === undefined) {
states[currentIndex] = initialValue;
}
//判断是普通的还是函数式更性
function setState(newValue) {
states[currentIndex] = typeof newValue === "function"
? newValue(states[currentIndex])
: newValue;
render();//触发一次重新渲染,让组件重新读取最新的 state
}
cursor++;
return [states[currentIndex], setState];
}
//模拟组件重新执行
function render() {
cursor = 0;
Component(useState);
}
return {
render,
};
}
//====测试=====//
function App(useState) {
const [count, setCount] = useState(0);
const [name, setName] = useState("Elemen");
console.log("render:", { count, name });
globalThis.add = () => setCount(count + 1);
globalThis.addSafe = () => setCount((prev) => prev + 1);
globalThis.changeName = () => setName("React");
}
const app = createRenderer(App);
app.render();
// render: { count: 0, name: 'Elemen' }
add();
// render: { count: 1, name: 'Elemen' }
addSafe();
// render: { count: 2, name: 'Elemen' }
changeName();
// render: { count: 2, name: 'React' }笔记记录
手写 useState 学习笔记
题目目标
实现一个简化版 useState,理解 React Hooks 最核心的状态保存方式。
这份实现不追求完整模拟 React,只关注三个关键点:
- 状态要保存在组件函数外面。
- 多个
useState要按照调用顺序依次读取。 setState后要触发组件重新执行。
核心原理
useState 的核心是:闭包 + 数组 + 索引 cursor。
const states = [];
let cursor = 0;states 数组用来保存每一次 useState 的状态。
cursor 用来记录当前执行到了第几个 useState。
组件每次重新执行时,函数内部的局部变量都会重新创建。如果状态直接写在组件函数内部,下一次渲染时状态就丢了。所以状态必须放在组件外层,通过闭包保存下来。
createRenderer 做了什么
createRenderer(Component) 接收一个组件函数,并在内部创建一套独立的状态系统。
function createRenderer(Component) {
const states = [];
let cursor = 0;
function useState(initialValue) {}
function render() {
cursor = 0;
Component(useState);
}
return {
render,
};
}这里的 states 和 cursor 都被 useState、render 闭包引用,所以它们不会随着组件函数执行结束而销毁。
useState 的执行过程
每次调用 useState 时,先记录当前索引:
const currentIndex = cursor;这个 currentIndex 很重要。它表示当前这个 useState 对应 states 数组里的哪个位置。
如果这个位置还没有值,说明是第一次渲染,就使用初始值:
if (states[currentIndex] === undefined) {
states[currentIndex] = initialValue;
}然后返回当前状态和更新函数:
return [states[currentIndex], setState];最后让 cursor++,下一个 useState 就会读取数组里的下一个位置。
为什么 setState 要记住 currentIndex
setState 不能直接使用 cursor,因为 cursor 会在每次渲染前被重置,也会随着 Hook 调用不断变化。
所以要在 useState 执行时保存一份固定的 currentIndex:
function setState(newValue) {
states[currentIndex] =
typeof newValue === "function"
? newValue(states[currentIndex])
: newValue;
render();
}这样每个 setState 都能更新自己对应的状态位置。
例如:
const [count, setCount] = useState(0);
const [name, setName] = useState("Elemen");setCount 永远更新 states[0]。
setName 永远更新 states[1]。
为什么 render 前要重置 cursor
组件重新执行时,Hook 要从第一个重新开始匹配。
function render() {
cursor = 0;
Component(useState);
}第一次渲染:
useState(0) -> cursor = 0 -> states[0]
useState("Elemen") -> cursor = 1 -> states[1]第二次渲染也必须保持同样顺序:
useState(0) -> cursor = 0 -> states[0]
useState("Elemen") -> cursor = 1 -> states[1]这就是 Hooks 依赖调用顺序的原因。
为什么 Hook 不能写在条件语句里
如果 Hook 写在条件语句里,某次渲染可能跳过一个 useState,后面的状态索引就会错位。
错误示例:
function App(useState) {
const [count, setCount] = useState(0);
if (count > 0) {
const [flag, setFlag] = useState(false);
}
const [name, setName] = useState("Elemen");
}当 count 从 0 变成 1 后,中间多执行了一次 useState,name 对应的数组位置就变了,状态会乱。
所以 Hooks 必须写在函数组件最外层,不能写在条件、循环或嵌套函数里。
setState 的两种写法
代码里支持两种更新方式。
第一种是直接传新值:
setCount(count + 1);第二种是函数式更新:
setCount((prev) => prev + 1);函数式更新会拿到当前最新状态:
typeof newValue === "function"
? newValue(states[currentIndex])
: newValue;这也是 React 里推荐用于依赖旧状态更新的写法。
测试流程
初始渲染:
app.render();输出:
render: { count: 0, name: 'Elemen' }调用 add() 后,更新 count:
add();输出:
render: { count: 1, name: 'Elemen' }调用 addSafe() 后,用函数式更新继续累加:
addSafe();输出:
render: { count: 2, name: 'Elemen' }调用 changeName() 后,更新第二个状态:
changeName();输出:
render: { count: 2, name: 'React' }这说明两个状态分别保存在不同数组位置,更新时不会互相影响。
这版实现的限制
这只是一个帮助理解原理的简化模型。
它没有实现 React 里的 Fiber、批量更新、异步调度、组件树、事件系统,也没有处理多个组件实例之间的状态隔离。
但它抓住了 useState 最重要的思想:状态存放在组件函数外部,组件重新执行时,通过稳定的 Hook 调用顺序把状态取回来。
总结
手写 useState 的关键不是 setState 函数本身,而是状态如何在多次组件执行之间保持稳定。
这个实现里:
states负责保存状态。cursor负责匹配 Hook 顺序。currentIndex让每个setState记住自己的状态位置。render通过重置cursor模拟组件重新执行。
理解这四点,就能理解 Hooks 为什么依赖调用顺序,也能理解为什么 Hooks 不能写在条件语句里。