useState
useState 的数据保存在 fiber components 节点上;通过链表的方式保存 hooks;所以 hooks 的顺序不能改变。
function Component() {
const [count, setCount] = useState(0); // hook 1
useEffect(() => {}); // hook 2
const [name, setName] = useState(""); // hook 3
}
miniState 实现
为了更好的理解useState建议有余力的同学可以手写下 miniState。
根据我的建议呢,手写之前需要先了解关于链表的知识点 看我的另一篇文章 前端需要了解的链表。
对链表有所了解之后就可以来看这段实现 useState 的代码了。 这里建议将代码 copy 到编辑器中运行打断点来梳理,当对整了解了整个运行过程后,可以自己开始手写实现,在手写的过程中感觉有哪里没有理解在回过头来看代码。
hooks可以理解为两个阶段 mount和update 分别对应初始化和用户操作,这里用isMount变量来作为区分;
useState本质上就是一个函数,同一个函数多次调用如何区分呢,这里主要靠workInprogressHook来作为指针,指向当前 hook;fiber 的memoizedState会记录 hooks 的链表顺序。
let isMount = true;
let workInprogressHook = null;
const fiber = {
stateNode: App,
memoizedState: null, // 记录state;
};
miniState 源码实现:
let isMount = true;
let workInprogressHook = null;
const fiber = {
stateNode: App,
memoizedState: null, // 记录state;
};
// 模拟hook 调度
const schedule = () => {
workInprogressHook = fiber.memoizedState;
const app = fiber.stateNode();
isMount = false;
return app;
};
const useState = (initialState) => {
let hook;
/**
* 更新hook链表的流程
*/
// 初始化阶段构建一个链表,fiber.memoizedState
// workInprogressHook 作为链表的指针 指向当前hook
if (isMount) {
hook = {
memoizedState: initialState,
next: null,
queue: {
pending: null,
},
};
// 表示第一次执行的第一个hook
if (!fiber.memoizedState) {
fiber.memoizedState = hook;
} else {
// 指针指向当前hook
workInprogressHook.next = hook;
}
workInprogressHook = hook;
}
// 获取当前hook,改变指针
else {
hook = workInprogressHook;
workInprogressHook = workInprogressHook.next;
}
/**
* 执行steState action
*/
// 获取上一次的值
let baseState = hook.memoizedState;
if (hook.queue.pending) {
// 第一个action;
let firstUpdate = hook.queue.pending.next;
// 循环执行环状链表
do {
const action = firstUpdate.action;
baseState = action(baseState);
// 更新指针
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending.next);
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
};
function dispatchAction(queue, action) {
let update = {
action,
next: null,
};
if (queue.pending === null) {
//自身与自身链接
update.next = update;
} else {
// 尾 和 首进行链接 u1-> u0
// update 是 u1 , queue.pending.next 是u0
// u1.next-> u0
update.next = queue.pending.next;
// queue.pending.next 是u0
// update 是 u1
// u0.next -> u1
queue.pending.next = update;
// 总结一下 形成环状链表
// u1.next -> u0 -> u0.next -> u1 -> u1.next
}
queue.pending = update;
// 触发app更新
setTimeout(() => {
schedule();
}, 0);
}
function App() {
const [num, updateNum] = useState(0);
const [num1, updateNum2] = useState(0);
return {
onclick() {
updateNum((num) => num + 1);
updateNum((num) => num + 2);
updateNum((num) => num + 3);
updateNum2((num) => num + 10);
},
};
}
window.app = schedule();
执行过程时序图:
sequenceDiagram
participant User
participant Schedule
participant App
participant useState
participant dispatchAction
participant Fiber
participant Hooks
Note over User, Hooks: 初始化阶段
User->>Schedule: 调用 schedule()
activate Schedule
Schedule->>Fiber: workInprogressHook = memoizedState (null)
Schedule->>App: 执行 fiber.stateNode()
activate App
App->>useState: 第一次调用 useState(0)
activate useState
useState->>Fiber: 检查 fiber.memoizedState (null)
useState->>Hooks: 创建 hook1 {memoizedState:0, queue.pending:null}
useState->>Fiber: fiber.memoizedState = hook1
useState->>App: 返回 [0, dispatchAction]
deactivate useState
App->>useState: 第二次调用 useState(0)
activate useState
useState->>Fiber: 检查 fiber.memoizedState (hook1)
useState->>Hooks: 创建 hook2 {memoizedState:0, queue.pending:null}
useState->>Hooks: hook1.next = hook2
useState->>App: 返回 [0, dispatchAction]
deactivate useState
App-->>Schedule: 返回 app 对象
deactivate App
Schedule->>Schedule: isMount = false
Schedule-->>User: 返回 app
deactivate Schedule
Note over User, Hooks: 用户交互阶段
User->>App: 调用 onclick()
activate App
App->>dispatchAction: updateNum(num=>num+1) (hook1.queue)
App->>dispatchAction: updateNum(num=>num+2) (hook1.queue)
App->>dispatchAction: updateNum(num=>num+3) (hook1.queue)
App->>dispatchAction: updateNum2(num=>num+10) (hook2.queue)
deactivate App
Note over User, Hooks: 更新调度阶段
dispatchAction->>Hooks: 为hook1创建u1,u2,u3
Hooks->>Hooks: 构建环形链表 u1→u2→u3→u1
Hooks->>Hooks: hook1.queue.pending = u3
dispatchAction->>Hooks: 为hook2创建u4
Hooks->>Hooks: 构建自环 u4→u4
Hooks->>Hooks: hook2.queue.pending = u4
dispatchAction->>Schedule: 异步调用 schedule()
Note over User, Hooks: 更新阶段
activate Schedule
Schedule->>Fiber: workInprogressHook = memoizedState (hook1)
Schedule->>App: 执行 fiber.stateNode()
activate App
App->>useState: 第一次调用 useState()
activate useState
useState->>Fiber: 获取当前hook (hook1)
useState->>Hooks: 读取hook1.queue.pending (u3)
loop 处理hook1的更新队列
useState->>Hooks: 执行action u1: 0+1=1
useState->>Hooks: 执行action u2: 1+2=3
useState->>Hooks: 执行action u3: 3+3=6
end
useState->>Hooks: hook1.memoizedState = 6
deactivate useState
也十分建议配合卡颂老师的文章和视频一起学习 文章链接 视频链接。
state 的异步和批量更新
上面的miniState只是模拟了 State 的部分实现,并不能完全反映真实代码中 state 的更新流程,接下来我们介绍异步和批量更新,这个 React 初学者最困惑的部分。
useState 的异步
通过上面的代码可以看出来 useState 数据的更新不是同步的,每次 state 的更新都会触发组件重新Render;
const [state, setState] = useState("1");
const click = () => {
setSate("18");
console.log(state); // 1
};
所以在我们改变了 state 的同时没有办法立刻通过获取state来拿到最新的值;这里在某些特殊极端场景下需要配合ref来拿到最新值;
useState 批量更新: 批量更新就是指,短时间内同时更新了多个 state,React 会将他们合并更新,避免多次触发 Redner;
const [state, setState] = useState("1");
const [state1, setState1] = useState("2");
const click = () => {
setSate("18");
setState1("3");
};
这里的批量更新在 React 17 和 18 版本中有很大的差异需要注意
React17 中批量更新在异步方法中是不会生效的,包括 promise 下面都会触发两次Render
const [state, setState] = useState("1");
const [state1, setState1] = useState("2");
const click = () => {
setTimeout(() => {
setSate("18");
setState1("3");
}, 0);
};
const [list, setList] = useState(1);
const [list2, setList2] = useState(2);
const fun = () => {
return new Promise((reslove) => {
reslove("sccuess");
});
};
const onClick = () => {
fun().then((res) => {
setList(list + 1);
setList2(list2 + 1);
});
};
React18中,对于这种场景做了很多支持,批量更新全部覆盖,上面的场景都会只render一次;
需要注意的是,需要使用React提供的全新apicreateRoot来启用新特性。
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')).render(<App />);
也提供了强制退出批处理的api,这里通常不需要使用(谨慎使用!)
import { flushSync } from 'react-dom';
flushSync(() => setCount(1)); // 强制同步提交
这两种批量更新的模式在React底层中分别对应 legacy模式 concurrent模式