在react17
中,对于在异步函数(例如promise
, setTimeout
)中调用多次setState
,会触发多次更新,但是在react18
中却没有这个问题。各位会不会感到好奇,带着这个问题,我们来看下面一个简单示例
const scheduleMicrotask = window.queueMicrotask;
const callbackPriority = 1;
// 存储是否有相同优先级的更新
const root = {
callbackPriority: null,
};
// 上一个更新
let lastupdate = null;
// 更新队列
let updateQueue = null;
// 状态
const state = {
count: 1,
};
// 模拟setState
function setState(payload) {
const update = {
payload,
next: null,
};
// 将更新组成一个链表
if (updateQueue === null) {
updateQueue = update;
lastupdate = update;
} else {
lastupdate.next = update;
lastupdate = update;
}
// 调度更新
scheduleUpdate();
}
function scheduleUpdate() {
if (root.callbackPriority !== null) {
return;
}
// 存储当前更新的优先级,
root.callbackPriority = callbackPriority;
scheduleMicrotask(() => {
updateRender();
});
}
function updateRender() {
let newCount = state.count;
let update = updateQueue;
// 去取出更新
while (update) {
newCount = update.payload;
update = update.next;
}
state.count = newCount;
// 重置
root.callbackPriority = null;
lastupdate = null;
updateQueue = null;
// 渲染dom
renderToDOM();
}
const box = document.querySelector('#box');
const btn = document.querySelector('#btn');
function renderToDOM() {
console.log('我更新了');
box.innerHTML = state.count;
}
const onClick = () => {
setState(2);
setState(5);
setState(10);
}
btn.addEventListener('click', onClick);
renderToDOM();
来看下效果
可以看到,当我们点击"提交按钮时",只打印了一次“更新了”。
实现思路
这里利用workloop
时间循环的机制
- 第一次调用
setState
的时候,产生一个update
,构建update
链表(模拟react
中的机制),然后在调用scheduleMicrotask
产生一个微任务。这个微任务需要再执行完onClick
事件后执行,此时更新聊表为
{
paylaod: 2,
next: null
}
- 第二次调用
setState
的时候,产生一个新update
,在上一个update
后追加
{
paylaod: 2,
next: {
payload: 5,
next: null
}
}
- 第三次调用
setState
的时候,同第二步
{
paylaod: 2,
next: {
payload: 5,
next: {
payload: 10,
next: 5
}
}
}
当执行完onClick
后,浏览器会从微任务队列中取出一个任务,放入执行栈,这个时候执行updateRender
,视图成功刷新
以上就是一个react setState的最简单实现
在react
中, setState
的实现复杂很多,分为同步任务(Sync lane
)和普通任务(default lane
),当我们在点击一个按钮的时候,同步调用state的时候就是同步任务,这个时候更新是一个微任务更新(会尽可能早的执行完更新,这个是react的一个优化,react任务鼠标等时间对于视图的更新是很急切的),而在包裹setTimeout
和promise
的setState,则更新lane
为default lane
此时是一个宏任务更新。
急切更新
下面一个例子中,当点击按钮时会产生一个Sync lane
的更新如下
import React, { useState } from 'react';
function App () {
const [count, setCount] = useState(0)
console.log('render')
const onClick = () => {
setCount((prev) => prev + 1)
setCount((prev) => prev + 1)
setCount((prev) => prev + 1)
}
return (
<div>
<div>{count}</div>
<button onClick={onClick}>Increment</button>
</div>
);
}
export default App;
源码我这就补贴了,感觉看起来很枯燥,这里我直接上图
在控制台中,我们会发现,render
只打印了一次。
这个时候有的同学会说:react17
也只打印一次。
的确是,这里确实表现是一样的,但是接下来的代码就有所区别了
普通更新
import React, { useState } from 'react';
function App () {
const [count, setCount] = useState(0)
const onClick = () => {
+ setTimeout(() => {
setCount((prev) => prev + 1)
setCount((prev) => prev + 1)
setCount((prev) => prev + 1)
+ }, 0)
}
return (
<div>
<div>{count}</div>
<button onClick={onClick}>Increment</button>
</div>
);
}
export default App;
运行上面的代码,你会发现,在react17
中,会打印三次,关于为何三次,我看了下源码是因为setTimeout
等异步函数,会跳出了批处理的更新范围,导致后面每次调用都会触发重新渲染
注意:react 17中需使用传统的
ReactDOM.render
的形式渲染,如果使用createRoot
的形式,表现跟react18一致
在react18
中,没有采用状态标识是否批量更新
,而是仍然采用任务调度的形式,对于异步任务里的 setState
, 将会产生一个 default lane
的更新,该更新被react任务不是很紧急的更新,会采用宏任务来调度
不同的地方为粉色区域,基本流程和上面同步差不多,知识任务的优先级不同