
一句话:写一个 mini React 只需要四块——
createElement造元素对象 → Fiber 树用「parent/child/sibling」三个指针串起来 →workLoop用「全局指针 + 时间片」实现可中断的深度优先遍历 → commit 阶段把 Fiber 树同步落到 DOM,再加一套「挂在 fiber 上的 Hook 链表」就有了useState与重渲染。
一、整体架构(先看清它长什么样)
mini-react 的四个模块(与真实 React 一一对应):
① createElement → 造「元素对象」{ type, props: { children, ... } }
② Fiber + workLoop → 「渲染阶段」:可中断的遍历,产出 fiber 树(含 effectTag)
③ commit → 「提交阶段」:不可中断地改 DOM(PLACEMENT/UPDATE/DELETION)
④ useState → Hook 挂在 fiber 上(链表/数组),setState 触发「新一次渲染」
⭐⭐ 三个「必须先想清楚」的设计决定(想错了后面全崩):
① 【DOM 什么时候创建?】
创建在「render 阶段」(fiber 生成时建好 dom 并 append 到父的 dom 上),
但【插入文档】延后到 commit(否则每渲染一个节点就触发一次真实布局)。
⚠️ 这意味着:render 过程中 DOM 是「在游离容器里」的——所以**可中断**。
② 【「更新」怎么表达?】
用一个全局 `wipRoot`(work in progress root):
① `render(element, container)` → 首次创建 wipRoot
② 每次 `setState` → 用「当前树(currentRoot)」克隆一个 wipRoot(带 alternate 指针)
③ workLoop 遍历 wipRoot,在「新旧 fiber 对比」时打上 effectTag
④ commit 完成后 `currentRoot = wipRoot`(⭐ 双缓存切换)
③ 【「可中断」怎么实现?】
把「递归」换成「while + 全局指针 nextUnitOfWork」:
每一次 `performUnitOfWork` 处理【一个 fiber】,然后检查 `shouldYield()`。
⭐ 只要遍历状态存在全局变量/对象里,就能随时「保存并返回」。
二、第一块:createElement(把 JSX 变成对象)
// mini-react.js —— 第 1 块:createElement
/** 文本子节点用特殊 type 标记(方便后面区分「文本节点」与「元素节点」) */
export const TEXT_ELEMENT = 'TEXT_ELEMENT';
/**
* ⭐ 把「children 变长参数」拍平成 props.children 数组
* 并把「字符串/数字」包装成「文本元素」(这样后面只需处理一种 child 形态)
*/
export function createElement(type, config, ...children) {
const props = { ...(config ?? {}) };
const normalizedChildren = children
.flat() // ⭐ 处理 {arr.map(...)} 产生的数组
.filter((c) => c !== null && c !== undefined && c !== false)
.map((child) =>
typeof child === 'object' ? child : createTextElement(String(child))
);
delete props.children;
if (normalizedChildren.length > 0) props.children = normalizedChildren;
return { type, props };
}
function createTextElement(text) {
return { type: TEXT_ELEMENT, props: { nodeValue: text } };
}
/** JSX 运行时(配 `/** @jsx createElement *\/` 或 babel 的 classic runtime) */
export const jsx = (type, props, key) => createElement(type, { ...props, key }, props?.children);
/** ============ 第 2 块:Fiber 结构与「创建 DOM」 ============ */
/** effectTag:本次渲染相对上一次的「变更类型」 */
const PLACEMENT = 'PLACEMENT';
const UPDATE = 'UPDATE';
const DELETION = 'DELETION';
/** 创建真实 DOM 节点(文本 / 元素 / 函数组件由调用方处理) */
function createDom(fiber) {
if (fiber.type === TEXT_ELEMENT) {
return document.createTextNode('');
}
const dom = document.createElement(fiber.type);
updateDom(dom, {}, fiber.props); // ⭐ 首次创建也要应用 props(含事件)
return dom;
}
/**
* ⭐⭐ 全篇最关键的函数之一:把「新旧 props」的差异应用到真实 DOM 上
* 这也是「为什么 React 能只改差异」的落地点
*/
function updateDom(dom, prevProps, nextProps) {
const isEvent = (key) => key.startsWith('on');
const isProperty = (key) => key !== 'children' && !isEvent(key);
const isGone = (prev, next) => (key) => !(key in next);
// ① 移除「旧事件」里被改掉的(移除 + 后面重新加)
Object.keys(prevProps)
.filter(isEvent)
.filter((key) => !(key in nextProps) || prevProps[key] !== nextProps[key])
.forEach((key) => {
const eventType = key.toLowerCase().substring(2); // onClick → click
dom.removeEventListener(eventType, prevProps[key]);
});
// ② 移除「旧的普通属性」
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach((key) => {
dom[key] = '';
});
// ③ 设置「新增/变化的属性」(⭐ 这里对 style 做了特殊处理)
Object.keys(nextProps)
.filter(isProperty)
.filter((key) => prevProps[key] !== nextProps[key])
.forEach((key) => {
if (key === 'style' && typeof nextProps[key] === 'object') {
Object.assign(dom.style, nextProps[key] ?? {});
// ⚠️ 省略了「删除旧 style 键」的处理(真实 React 会做 diff)
} else {
dom[key] = nextProps[key];
}
});
// ④ 添加「新事件」
Object.keys(nextProps)
.filter(isEvent)
.filter((key) => prevProps[key] !== nextProps[key])
.forEach((key) => {
const eventType = key.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[key]);
});
}
三、第三块:可中断的 workLoop
/** ============ 第 3 块:渲染阶段(可中断的深度优先遍历) ============ */
/** ⭐ 全局遍历状态(能存在这里,才谈得上「可中断」) */
let nextUnitOfWork = null; // 下一个要处理的 fiber
let wipRoot = null; // 正在构建的根(work in progress)
let currentRoot = null; // 上一次已提交的根(用于 diff)
let deletions = []; // ⭐ 本次渲染要删除的 fiber(commit 时统一处理)
/** 内建的最小调度:时间片 + 让出(思想与真实 React 的 Scheduler 一致,见 4.17) */
const FRAME_YIELD_MS = 5;
function shouldYield() {
return performance.now() - frameStart > FRAME_YIELD_MS;
}
let frameStart = performance.now();
/** 入口:把元素渲染到容器里 */
export function render(element, container) {
wipRoot = {
dom: container,
props: { children: [element] },
alternate: currentRoot, // ⭐ 双缓存:指向上一棵树
};
deletions = [];
nextUnitOfWork = wipRoot;
frameStart = performance.now();
scheduleWork();
}
/** 用 rAF 驱动(真实 React 用 MessageChannel,见 4.17) */
function scheduleWork() {
requestAnimationFrame(() => {
frameStart = performance.now();
workLoop();
});
}
function workLoop() {
// ⭐⭐ 可中断的关键:每处理一个 fiber 检查一次「是否该让出」
while (nextUnitOfWork && !shouldYield()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (nextUnitOfWork) {
scheduleWork(); // 本帧没跑完 → 下一帧继续(断点续跑)
} else if (wipRoot) {
commitRoot(); // 全部处理完 → 提交
}
}
/**
* ⭐⭐ 处理【一个】fiber:三件事
* ① 给它建好 dom 并挂到父 dom 上
* ② 对比新旧 children,生成子 fiber(并打 effectTag)
* ③ 返回「下一个要处理的 fiber」(child → sibling → 上溯找 uncle)
*/
function performUnitOfWork(fiber) {
// ① 函数组件:先执行它拿到 children(不创建 dom)
const isFunctionComponent = typeof fiber.type === 'function';
if (isFunctionComponent) {
updateFunctionComponent(fiber);
} else {
updateHostComponent(fiber);
}
// ③ 返回下一个工作单元(深度优先)
if (fiber.child) return fiber.child;
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) return nextFiber.sibling; // 有兄弟 → 处理兄弟
nextFiber = nextFiber.parent; // 没有 → 回到父,继续找父的兄弟
}
return null; // 回到根 → 结束
}
/** ============ 第 3.5 块:两类组件的处理 ============ */
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
// ⭐ 注意:这里【不】把 dom append 到父 dom(延迟到 commit),
// 这样 render 阶段可中断、可丢弃。这里我们采用「render 阶段建 DOM、
// commit 阶段插入」的折中(Didact 的原始做法是在这里 append,
// 但那样会「在 render 期间改动真实 DOM」,违背可中断的前提)。
const elements = fiber.props.children ?? [];
reconcileChildren(fiber, elements);
}
function updateFunctionComponent(fiber) {
wipFiber = fiber; // ⭐ 当前正在渲染的 fiber(Hook 需要它)
hookIndex = 0; // ⭐ 重置 Hook 游标
wipFiber.hooks = [];
const children = [fiber.type(fiber.props)]; // 执行组件函数
reconcileChildren(fiber, children.flat().filter(Boolean));
}
/**
* ⭐⭐ 协调(reconcile):把「新元素数组」与「旧 fiber 子链」对比,
* 复用能复用的 fiber,标记新增/删除
*/
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate?.child ?? null; // 旧子链的头
let prevSibling = null;
while (index < elements.length || oldFiber) {
const element = elements[index];
let newFiber = null;
// ⭐ 判据:type 相同 → 复用;type 不同 → 新建 + 删旧的
const sameType = element && oldFiber && element.type === oldFiber.type;
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom, // ⭐ 复用真实 DOM
parent: wipFiber,
alternate: oldFiber, // ⭐ 双缓存指针
effectTag: UPDATE,
};
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: PLACEMENT,
};
}
if (oldFiber && !sameType) {
oldFiber.effectTag = DELETION; // ⭐ 标记删除
deletions.push(oldFiber);
}
if (oldFiber) oldFiber = oldFiber.sibling;
// 串成「child / sibling」链
if (index === 0) {
wipFiber.child = newFiber;
} else if (element && prevSibling) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
四、第四块:useState(Hook 与重渲染)
/** ============ 第 4 块:Hook 系统(useState / useReducer) ============ */
let wipFiber = null; // 当前正在渲染的函数组件 fiber
let hookIndex = null; // ⭐ Hook 游标(顺序稳定的保证)
/**
* ⭐⭐ Hook 的最小实现:
* · 每个 fiber 上挂一个 hooks 数组
* · 每次渲染从头开始,用 hookIndex 逐个「按顺序」取
* → 这就是「Hook 不能写在条件里」的根本原因:顺序必须每次一致
*/
export function useState(initial) {
return useReducer(null, initial);
}
function useReducer(reducer, initial) {
const oldHook =
wipFiber.alternate?.hooks?.[hookIndex] ?? // ⭐ 从上一棵树取旧 Hook
{ state: initial, queue: [] };
const hook = { state: oldHook.state, queue: [] };
// ⭐ 消费「上次 setState 推入的更新队列」
oldHook.queue.forEach((action) => {
hook.state = reducer ? reducer(hook.state, action) : typeof action === 'function' ? action(hook.state) : action;
});
const setState = (action) => {
hook.queue.push(action);
// ⭐⭐ 触发重渲染:以「当前树」为基准,构造一棵新的 wipRoot
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot, // 让 diff 能对比到旧树
};
nextUnitOfWork = wipRoot;
deletions = [];
scheduleWork();
};
wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state, setState];
}
/** ============ 第 5 块:commit(把 diff 结果同步落到 DOM) ============ */
function commitRoot() {
// ① 先处理删除(⭐ 必须在新增/更新之前,否则可能出现「先插入再删除同位置」的闪烁)
deletions.forEach(commitWork);
// ② 递归提交
commitWork(wipRoot.child);
// ⭐ ③ 双缓存切换:新树成为「当前树」
currentRoot = wipRoot;
wipRoot = null;
}
function commitWork(fiber) {
if (!fiber) return;
// ⭐ 找到「最近的有 dom 的祖先」(函数组件的 fiber 没有 dom)
let domParentFiber = fiber.parent;
while (domParentFiber && !domParentFiber.dom) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber?.dom;
switch (fiber.effectTag) {
case PLACEMENT:
if (fiber.dom && domParent) {
domParent.appendChild(fiber.dom);
} else if (!fiber.dom && domParent) {
// 函数组件没有 dom → 把它的子节点挂上去
// (由子节点自己的 PLACEMENT 处理,这里不做事)
}
break;
case UPDATE:
if (fiber.dom) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}
break;
case DELETION:
commitDeletion(fiber, domParent);
return; // ⭐ 删除后不再遍历子树(已整棵移除)
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
// ⭐ 函数组件没有 dom → 递归找它的子节点删
commitDeletion(fiber.child, domParent);
}
}
/** ============ 使用示例 ============ */
import { render, createElement as h, useState } from './mini-react';
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
return h(
'div',
{ style: { padding: '16px', fontFamily: 'system-ui' } },
h('h2', null, `计数:${count}`),
h('button', { onClick: () => setCount((c) => c + step) }, `+${step}`),
h('button', { onClick: () => setCount((c) => c - step) }, `-${step}`),
h('button', { onClick: () => setStep((s) => s + 1) }, `步长 ${step} → ${step + 1}`),
h('ul', null, ...Array.from({ length: Math.min(count, 5) }, (_, i) => h('li', { key: i }, `第 ${i + 1} 项`))),
);
}
render(h(Counter, null), document.getElementById('root'));
⭐⭐ 这个实现里「与真实 React 对应」的六个点(面试价值最高的部分):
① 【`performUnitOfWork` 的「深度优先」】
返回「child → sibling → 上溯 uncle」——这正是 Fiber 把「递归」变成「循环」的手法
(见 4.2)。真实 React 里对应 `beginWork` / `completeUnitOfWork`。
② 【`effectTag` 三值】
`PLACEMENT`(新增)、`UPDATE`(更新)、`DELETION`(删除)
→ 真实 React 里是 `flags`(二进制位运算,可组合,如 Placement | Update)。
③ 【`deletions` 数组「延迟删除」】
删除必须在「所有新增/更新之前」执行,否则会「先插入后删除」产生闪烁。
真实 React 同样把 `childDeletions` 收集起来,在 mutation 阶段优先处理。
④ 【`alternate` 双缓存】
每次更新都「基于 currentRoot 建新树」,并把 `alternate` 指向旧树,
从而能在 reconcile 时「拿到旧的 props/state 做对比」。
→ 这就是「双缓存(double buffering)」的最小形态(见 4.2)。
⑤ 【`hookIndex` 顺序】
Hook 存在「fiber.hooks 数组」里、靠「下标」定位 →
⭐ 这就是「Hook 必须无条件按序调用」的机制原因。
⑥ 【「render 可中断 + commit 不可中断」】
`workLoop` 里每处理一个 fiber 检查 `shouldYield()`;
而 `commitRoot` 是「一口气递归到底」——**没有让出点**。
→ 这就是「渲染阶段可以被打断、提交阶段不能」的最小复现(见 4.17)。
⭐⭐⭐ 这个实现「故意没做」的五件事(理解边界才算真懂):
① 【没有优先级/车道】
真实 React 有 Lane 模型(可以「被打断后重来」「高优先级插队」);
这里只有一个「时间片」,被打断后【从断点继续】而不是「丢弃重来」。
⚠️ 差别很重要:真实的并发渲染会「丢弃 wip 树、用更高优先级重渲染」,
而简单实现只是「分帧」。这也是「为什么 dev StrictMode 会双调用」的根源。
② 【没有 diff 的性能优化】
真实 React 有「同层比较、type 变了整棵重建、key 匹配」;
这里做了「type 相同就复用」的简化版——⚠️ 我甚至没实现 key 的复用
(上面 `Array.from` 的例子用 index 当 key,会「就地更新」而不是「移动」)。
③ 【没有合成事件委托】
这里是「一个元素一个 addEventListener」,真实 React 是「全部委托到根节点」,
并用「事件冒泡 + 合成事件对象」统一(见 4.9)。
④ 【没有批量更新】
每次 `setState` 都会立刻 `scheduleWork()`(真实 React 会「合并同一个事件里的多次 setState」)。
⭐ 改进:把「触发调度」放进微任务(`queueMicrotask`),自然就有批处理了。
⑤ 【没有 Context / Effect / memo / Suspense】
`useEffect` 本可以只加 20 行(在 commit 后执行 fiber.effects 并保存清理函数),
Suspense 见 11.12。
// ✅ 「④ 批量更新」的最小改法(让一个事件里的多次 setState 只调度一次)
let isScheduled = false;
function requestUpdate() {
if (isScheduled) return;
isScheduled = true;
queueMicrotask(() => { // ⭐ 等当前同步代码跑完
isScheduled = false;
scheduleWork();
});
}
// 把 setState 里的 scheduleWork() 换成 requestUpdate() 即可
五、本篇特有的坑
// ① 在 render 阶段直接往 document 里 append(破坏「可中断」的前提)
domParent.appendChild(fiber.dom); // ❌ 在 performUnitOfWork 里做
// ⚠️ 一旦让出,用户就看到「半截 DOM」→ 必须「render 建 DOM、commit 插入」
// ② 忘了「函数组件没有 dom」这件事
domParent.removeChild(fiber.dom); // ❌ fiber.dom 可能是 null(函数组件)
// ✅ 先「向上找最近的有 dom 的祖先」/「向下找第一个有 dom 的后代」
// ③ 删除操作没「提前执行」
// ⚠️ 如果先处理 PLACEMENT 再处理 DELETION,
// 可能出现「同一位置先插入新节点、再删除旧节点」→ 视觉闪烁 + 焦点丢失
// ✅ deletions 数组 + 在 commit 最开始统一处理
// ④ Hook 用「对象 + key」存储(顺序错乱时无声出错)
const hooks = {}; if (hooks[someKey]) ... // ❌ 无法检测「Hook 顺序变了」
// ✅ 用数组 + 下标(配合「长度不一致」的检测能提前报警)
// ⑤ 在「循环/条件」里调用 useState
if (cond) useState(0); // ❌ hookIndex 会错位
// ⭐ 机制原因:Hook 按「调用顺序的下标」定位,少调一次 → 后面全错位
// ⑥ 忘了「每次渲染都要重置 hookIndex」
// ⚠️ 不重置的话,第二次渲染就会从「数组末尾」接着写 → 状态错乱
// ✅ 在 updateFunctionComponent 开头 `hookIndex = 0`
// ⑦ setState 里直接用「闭包捕获的 state」做计算
setCount(count + 1); // ⚠️ 连续调用会丢失更新
// ✅ setCount((c) => c + 1)(用函数式更新,读「最新的 state」)
// ⑧ `updateDom` 里「删除属性」用了 `dom[key] = ''`
// ⚠️ 对某些属性(如 `value`、`checked`)会留下奇怪的值
// ✅ 对「布尔属性」用 `removeAttribute`;或者干脆「重建节点」(真实 React 有时就这么做)
// ⑨ 事件监听「每次都重新绑定」但没有解绑
// ⚠️ 我的实现里通过「比较 prev/next 的函数引用」来解绑 + 重绑;
// 如果每次传的 arrow function 都是新的(内联函数),就会「每帧都解绑+重绑」
// ✅ 真实 React 用「事件委托到根节点」规避了这个问题
// ⑩ 用「数组下标当 key」(本实现的 reconcile 没实现 key 匹配,问题更严重)
// ⚠️ 列表重排序时会「就地更新内容」→ 组件状态错位、动画错乱
// ✅ 真实 React 用 key 做「移动」而不是「重建」
// ⑪ 忘了「在 commit 后清理 wipRoot」
// ⚠️ 不清的话,下一次渲染会「基于已经提交的旧 wipRoot」→ 状态错乱
// ✅ `currentRoot = wipRoot; wipRoot = null`
// ⑫ 用「同步递归」写 workLoop
function workLoopSync(fiber) { ... workLoopSync(fiber.child); } // ❌ 无法中断
// ✅ 用「全局指针 + while」把递归状态「外化」
// ⑬ 认为「mini React 能当生产框架用」
// ⚠️ 它没有「错误边界、Suspense、Context、并发优先级、SSR、合成事件、
// 受控组件、ref、Portal、Fragment、key 复用、批量更新、内存回收…」
// ✅ 它的价值是「理解机制」,不是「替代 React」
// ⑭ 在「让出之后」直接改 fiber 上的字段(导致「半更新」状态可见)
// ⚠️ 中断点之后如果另一半数据没准备好,别把「部分结果」暴露给外部
// ✅ 所有对外可见的改动都留在 commit 阶段
// ⑮ 忘记「fiber.dom 必须复用」(否则每次更新都重建 DOM)
newFiber.dom = createDom(newFiber); // ❌ 丢失输入框内容/焦点/滚动位置
// ✅ 复用 `oldFiber.dom`,只更新变化的属性
// ⑯ 「文本节点」没有走 updateDom
// ⚠️ 文本节点的内容变化要单独处理(`dom.nodeValue = props.nodeValue`)
// ✅ 在 updateDom 开头分支处理 TEXT_ELEMENT
// ⑰ `reconcileChildren` 里 `oldFiber` 的推进写错
// ⚠️ 如果「新增元素多于旧 fiber」,循环必须还能继续(靠 `|| oldFiber` 的条件)
// ✅ `while (index < elements.length || oldFiber)`
// ⑱ 忘了「alternate 可能在半途中被丢弃」
// ⚠️ 真实 React 里「被打断的 wip 树」会被丢弃(所以 render 必须纯);
// 简单实现里「从断点继续」,因此**恰好掩盖了不纯的 render**
// → 这解释了「为什么简单实现跑得好好的代码,在真实 React 里会出问题」
// ⑯ 的完整处理(文本节点的更新)
function updateDom(dom, prevProps, nextProps) {
if (dom instanceof Text) {
if (prevProps.nodeValue !== nextProps.nodeValue) {
dom.nodeValue = nextProps.nodeValue ?? '';
}
return; // ⭐ 文本节点不处理其它属性
}
// ...其余逻辑
}
六、面试延伸
- 「手写一个 mini React,你会怎么设计?」
四块:①createElement——把 JSX 变成{ type, props: { children } }的纯对象,并把字符串子节点包装成「文本元素」(统一 child 形态);② Fiber 遍历——用parent/child/sibling三个指针把树串成链表,performUnitOfWork每次处理一个 fiber 并返回「下一个」(child → sibling → 上溯 uncle),这使遍历状态可以外化到全局指针,从而可中断;③ commit——把effectTag(PLACEMENT/UPDATE/DELETION)同步落到 DOM,其中删除必须先执行(避免闪烁),完成后currentRoot = wipRoot做双缓存切换;④ Hook——在 fiber 上挂hooks数组,用hookIndex按顺序取用,setState推入更新队列并基于 currentRoot 构造新 wipRoot 触发重渲染。 -
「为什么 Fiber 用链表而不是递归?」
因为递归的调用栈无法保存。Fiber 把「树」变成「带 parent/child/sibling 指针的链表」,遍历状态就落在「一个全局指针nextUnitOfWork」上——于是可以「随时保存并返回」,等下一帧再接着跑。这就是「可中断(时间切片)」的前提。⭐ 反过来看:这也是「render 阶段可能被丢弃并重来」的根源(真实 React 在有更高优先级更新时,会丢弃半成品树用新优先级重渲染)→ 所以 render 必须纯。 -
「
effectTag是什么?为什么要单独收集deletions?」
effectTag记录「这个 fiber 相比上一次是什么变更」:PLACEMENT(新增)、UPDATE(更新)、DELETION(删除)——真实 React 用位运算的flags(可组合,如「既要插入又要更新 ref」)。单独收集deletions的原因是「顺序」:删除必须在「新增/更新之前」执行,否则同一位置会出现「先插入新节点、再删除旧节点」的中间态(视觉闪烁、焦点丢失、动画错乱)。所以实现上把要删的 fiber 收集进数组,在 commit 最开始统一处理。 -
「Hook 为什么不能写在条件里?」
因为 Hook 是按「调用顺序的下标」定位的:实现上每个 fiber 挂一个hooks数组,每次渲染从头开始、用hookIndex逐个取(fiber.hooks[hookIndex])。如果某次渲染少调用了一个 Hook,后面的 Hook 就会全部错位(useState拿到的是别的 Hook 的 state)→ 状态彻底错乱。⭐ 这也解释了「为什么它必须在函数组件顶层无条件调用」——它依赖的是「每次渲染的调用序列完全一致」。 -
「为什么 render 阶段可以中断,commit 阶段不能?」
render 阶段只做「计算与创建 fiber」,产物在 wip 树里(还没接到真实文档上,或者只是「游离的 DOM」)——所以可以随时暂停/丢弃。而 commit 阶段在修改真实 DOM,如果做到一半被打断,用户会看到不一致的界面(列表删了一半、样式只应用了一部分)。⭐ 所以 React 的取舍是「渲染可以慢慢算,提交必须一口气做完」。这也是为什么「commit 阶段的耗时」直接决定「这一帧会不会掉帧」。 -
「你的实现里
setState每次都会立刻重新调度渲染,真实 React 也是这样吗?」
不是。真实 React 会批处理:在同一个事件处理里调 3 次setState只渲染一次——机制是「SyncLane的渲染被放进微任务(scheduleMicrotask(flushSyncCallbacks)),等当前同步代码跑完再统一渲染」(见 4.17)。在 mini 实现里只需把「触发调度」换成微任务(queueMicrotask)就能得到等价的批处理效果。⭐ 这个对比很能说明「批处理不是特例,而是调度的自然结果」。 -
「mini React 与真实 React 最大的差距在哪?」
五处:① 优先级/车道——真实 React 有 Lane 模型,可以「被打断后丢弃重来」与「高优先级插队」;简单实现只有「时间片分帧、断点续跑」。② diff 的完整性——真实 React 有 key 匹配 + 移动复用(getPlacement等逻辑),简单实现只有「type 相同就复用」。③ 合成事件——真实 React 把事件委托到根节点,简单实现是「一个元素一个监听器」(内联函数会导致反复解绑/重绑)。④ 功能覆盖——Context/Effect/ref/Portal/Fragment/Suspense/错误边界/受控组件/SSR 都没有。⑤ 内存与边界处理——真实 React 有大量细节(属性白名单、style diff、dangerouslySetInnerHTML 等)。 -
「如果给你的 mini React 加
useEffect,你会怎么做?」
三步:① 在 Hook 里登记——useEffect(create, deps)时把{ create, deps, cleanup }推到wipFiber.hooks(与useState共用同一套「按下标取」的机制,所以要共用一个hookIndex);② 判断是否需要重跑——比较deps与上一次的deps(逐项Object.is;deps为undefined时每次都跑,[]时只在挂载时跑);③ 在 commit 之后执行——把「需要跑的 effect」收集起来,在commitRoot结束后统一执行(⭐ 真实 React 的 passive effects 是「异步」执行的,用微任务/调度器延迟,以免阻塞提交后的绘制;而useLayoutEffect是在 commit 内同步执行的,见 2.17)。
一句话速记
mini React 的四块是「
createElement(JSX → 纯对象 + 文本节点包装)→ Fiber 链表(parent/child/sibling)+workLoop(全局指针nextUnitOfWork+shouldYield()→ 可中断的深度优先遍历,返回顺序是 child → sibling → 上溯 uncle)→ commit(effectTag= PLACEMENT/UPDATE/DELETION,删除最先执行,结束做currentRoot = wipRoot双缓存切换)→ Hook(fiber.hooks 数组 +hookIndex按顺序取,setState推队列并基于 currentRoot 造新 wipRoot)」;三个「为什么」:Fiber 用链表是因为递归的栈无法保存(因此 render 可能被丢弃重来 → 必须纯)、Hook 不能写在条件里是因为它按「调用顺序的下标」定位、删除要单独收集是因为它必须先于插入执行;与真实 React 的差距是「Lane 优先级(会丢弃重来)、key 复用、合成事件委托、批处理(微任务)、Context/Effect/Suspense」——加useEffect只需「登记 → 比 deps → commit 后执行」。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!