这是对上一篇的补充。上一篇 → 2.2.5、React 是如何工作的?(核心)
1、setState 是怎么做到让页面更新的?
这个问题可以拆解为以下两个问题?
Q1:什么是”页面更新“?
A:
即进入 render-commit 流程。那么如何出发 render-commit呢?
调用scheduleUpdateOnFiber ,这个方法会找到相应组件对应的 fiber 节点,给 fiber 节点打上lane优先。然后触发 workloop。
Q2: setState如何触进入发 redner-commit 流程?
A2:
当你调用 setState 时,React 实际上是调用了内部的 scheduleUpdateOnFiber 函数。
- 它会找到当前组件对应的 Fiber 节点。
- 给这个 Fiber 节点计算一个优先级 (Lane)。
- 从这个节点开始,向上遍历直到
HostRoot,告诉 React:“我家出事了,需要调度一次更新”。 - (优先级策略)如果你设置的新值和旧值是同一个(
Object.is(oldState, newState)为true),React 会进行 Bailout (急救退出),直接跳过更新,连Render阶段都不进。
2、其他触发更新方式是如何工作的?
A:
1、useReducer 等设置 state 的,和setState一样。
2、props 变化,和setState一样。在标记该fiber节点的 lane优先级时,会默认吧所有子组件的 lane标记需要更新。
3、context 变化。在provider更新时,标记 lane 环节,找到依赖该context的 fiber,打上lane更新标记。
4、key 变了。销毁重建。
3、setState / useState 为什么是批处理的?
A:
1、每次 setState 都会触发scheduleUpdateOnFiber方法。
2、scheduleUpdateOnFiber 方法会将setState 的变化放到一个队列里。
3、scheduleUpdateOnFiber 实现这个调度的逻辑如下:
function scheduleUpdateOnFiber(updateState) {
if(是否有任务在排队 == no) {
1. 发起一个微任务(通过 Scheduler 或 queueMicrotask)
微任务预订render 的执行
}
2. 将updateState 放到要更新的updateQueue链表中
}
当主线程的宏任务(比如点击事件代码)执行完,JS 引擎开始清空微任务队列:
React 的渲染任务启动(Render Phase)。
Process Queue: 此时,React 才会去遍历那个长长的 updateQueue 链表。
Merge: 按照顺序计算 state (baseState + Update1 + Update2 ...)。
Result: 得到最终的 memoizedState。4、思考
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
// 结果是 1
const handleClick = () => {
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
};
// 结果是 34、React 的合成事件是怎么工作的?
A:
原生冒泡 → Root接管 → 找Fiber节点 → 收集路径上的回调 → 挨个执行
1、React 在id=’root’设置了事件委托,但点击button 时,原生事件冒泡到root;
2、root 的委托事件,通过冒泡中 dom 的数据,在fiber tree中找到点击的 fiber node;
3、在fiber tree 中模拟冒泡,收集其中的onClick等事件。
4、在 react 内部,模拟捕获、冒泡事件。
追问Q: 在onClick中添加e.stopPropagation()是怎么阻止事件冒泡的?
A:
1、原生事件依然冒泡到react的委托事件;
2、react 的查找 Fiber node和收集onClick等不变;
3、内部模拟时,当发现有e.stopPropagation(),打标,停止冒泡后续事件的执行。
5、React 中 ref 是怎么工作的?
A:
1、本质是一个对象,即目标对象 DOM 的引用。
{
current: null // 或初始值
}2、在 react 的 render+commit阶段,进行赋值。
render 阶段:ref的值不变。
commit 阶段:
- before 阶段:-。
- 修改 DOM 阶段:如果 DOM 销毁,ref.current = null;
- layout阶段:设置ref的值为目标 DOM 的值;
6、Suspense 是怎么工作的?
Q:
在数据没到位时,
这个问题变成,Suspense 如何监听一个异步数据到位?
A:
异步数据的结果回调,然我们可以想到Promise (pending,fulfilled)。
那么实现就很好理解了。
核心是在react render阶段轮训,如果读取到的数据包返回是一个promise,则向上查找最近的 Suspense,渲染fallback。
// 伪代码
// 这是一个资源包装器
function wrapPromise(promise) {
let status = "pending";
let result;
// 立即开始执行 promise
let suspender = promise.then(
(r) => {
status = "success";
result = r;
},
(e) => {
status = "error";
result = e;
}
);
return {
read() {
if (status === "pending") {
// 🔥 核心魔法:数据没好,抛出 Promise!
throw suspender;
} else if (status === "error") {
// 数据错了,抛出 Error
throw result;
} else if (status === "success") {
// 数据好了,直接返回结果
return result;
}
}
};
}
// 组件中使用
const resource = wrapPromise(fetchData());
function UserProfile() {
// 这里看起来像同步代码!
// 如果数据没好,这行代码会 throw,导致后面的代码根本不会执行
const user = resource.read();
return <h1>{user.name}</h1>;
}
Render 时读数据
↓
没准备好就 throw Promise
↓
Suspense 接管 → fallback
↓
Promise resolve → 重渲染
// 5个步骤
Step 1:组件在 Render 阶段读取资源
const data = resource.read()
Step 2:数据未就绪 → 抛出 Promise
throw promise
Step 3:React 捕获 Promise(关键点)
React 在 Render 阶段 捕获到这个 Promise 后:
✔ 标记当前 Fiber 为 Suspended
✔ 向上查找最近的 <Suspense>
Step 4:切换到 fallback UI
中断当前子树渲染
显示 Suspense fallback
Step 5:Promise resolve → 重新调度渲染
当 Promise resolve:
React 重新调度该 Fiber
再次进入 Render 阶段
正常渲染真实 UI