这是对上一篇的补充。上一篇 → 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 函数。

  1. 它会找到当前组件对应的 Fiber 节点。
  2. 给这个 Fiber 节点计算一个优先级 (Lane)。
  3. 从这个节点开始,向上遍历直到 HostRoot,告诉 React:“我家出事了,需要调度一次更新”。
  4. (优先级策略)如果你设置的新值和旧值是同一个(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);
};
// 结果是 3

4、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 阶段:

  1. before 阶段:-。
  2. 修改 DOM 阶段:如果 DOM 销毁,ref.current = null;
  3. layout阶段:设置ref的值为目标 DOM 的值;

6、Suspense 是怎么工作的?

Q:

在数据没到位时,会展示fallback的内容(如)。当数据渲染到位了,就渲染数据。

这个问题变成,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 5Promise resolve → 重新调度渲染
Promise resolve:
React 重新调度该 Fiber
再次进入 Render 阶段
正常渲染真实 UI