📝 docs(code-reader): React 钩子卡片原型去掉逻辑分析行,加调用树按钮

This commit is contained in:
cruldra committed 2026-10-16 19:29:15 +11:00
1 parent 9fef9b36a9
commit 78d0c9dada
4 files changed
+2037 -6493

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 KiB

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

+40 -85
View File
@@ -1,7 +1,8 @@
// 代码阅读 React 钩子卡片:一个 VS Code 窗口里左边是编辑器、右边是代码阅读预览,每张卡片和它那段源码的第一行对齐,
// 卡片比源码高时,编辑器在那段源码下面留空,下一段源码再和下一张卡片对齐。
// 每处钩子调用都是同一种「钩子」卡片:钩子 / 类型参数 / 参数 / 返回四行照钩子签名排,各类钩子只在后面追加自己的行;
// 左边的钩子图标、描边所有钩子一样,淡底按来源分,React 自带的钩子左边再并排一个作用图标;几处位置、几处改动都排成表。
// 每处钩子调用都是同一种「钩子」卡片,只有钩子 / 类型参数 / 参数 / 返回四行,照钩子签名排;
// 左边的钩子图标、描边所有钩子一样,淡底按来源分,React 自带的钩子左边再并排一个作用图标;
// 卡片不分析钩子里写了什么,返回值里有函数的,右上角的调用树按钮查它在哪调用。
// 样例源码 `src/chat/Panel.tsx` 是手写的,两张画板共用:一张画第 1–21 行,一张画第 23–60 行;钩子来自 React、第三方库、项目自己各有一处。
// 颜色、窗口和卡片画法取自同目录 react.js(VS Code Dark Modern,卡片淡底 7%、描边 35%),函数小框取自同目录 rust-chain.js;
// 卡片里的译文照 vscode/src/codeReader/translators/typescriptZh.ts 的中文直译,渲染大纲的说法照 vscode/src/codeReader/rules/react.ts。
@@ -574,13 +575,23 @@ function group(parent, segs, color, bg) {
// ─── 卡片 ────────────────────────────────────────────────────────────────────
/** 卡片右上角:问 AI、编辑。 */
function tools(parent) {
/**
* 卡片右上角:问 AI、编辑;给了 `calls` 再加调用树,同 webview 的 BlockTools:图标后面跟调用次数的小圆标
* (徽标底色、字色取 Dark Modern 的 badge)。返回这一排的宽度。
*/
function tools(parent, calls) {
const bar = row(parent, "Tools", { width: "fit_content", height: 20, gap: 2 });
for (const name of ["sparkles", "pencil"]) {
const button = row(bar, name, { width: 22, height: 20, justify: "center", radius: 4 });
icon(button, name, C.desc, 14);
}
if (!calls)
return 22 * 2 + 2;
const tree = row(bar, "git-branch", { width: "fit_content", height: 20, padding: [0, 4], gap: 1, radius: 4 });
icon(tree, "git-branch", C.desc, 14);
const badge = row(tree, "Count", { width: 14, height: 14, justify: "center", radius: 7, fill: "#616161" });
text(badge, String(calls), { size: 9, weight: 700, color: "#f8f8f8", lh: 14 });
return 22 * 2 + 2 + 2 + (4 + 14 + 1 + 14 + 4);
}
/**
@@ -618,7 +629,7 @@ function conceptIcon(parent, item) {
/**
* 一张卡片:折叠箭头(有子块才有:`open` 展开、`closed` 收起)、概念图标、内容、右上角两个按钮。
* 概念图标默认是种类图标一个,给了 `icons` 就并排画这几个(`{name, color, tip, bubble?}`)。
* 淡底默认用种类色,给了 `tint` 用它(钩子卡片按来源分底色)。
* 淡底默认用种类色,给了 `tint` 用它(钩子卡片按来源分底色)。给了 `calls` 右上角多一个调用树按钮。
* `w` 是卡片外宽;返回内容区和内容区的宽度,属性行、表格按它断行。
*/
function card(parent, kind, w, o) {
@@ -640,9 +651,9 @@ function card(parent, kind, w, o) {
const concepts = row(node, "Concepts", { width: "fit_content", height: 20, gap: 2 });
for (const item of icons) conceptIcon(concepts, item);
const content = col(node, "Content", { gap: 3 });
tools(node);
const toolsW = tools(node, opt.calls);
const iconsW = icons.length * 20 + (icons.length - 1) * 2;
return { node, content, w: w - 18 - 14 - iconsW - 46 - 30, bg, labelW: opt.labelW || 76 };
return { node, content, w: w - 18 - 14 - iconsW - toolsW - 30, bg, labelW: opt.labelW || 76 };
}
/** 属性表的一行:左边说明色的标签,右边值。`value` 是着色的段、字符串,或自己画的函数 `(frame, width)`。 */
@@ -799,7 +810,7 @@ function reducerCard(parent, w) {
}
function hookCard(parent, w) {
const ctx = card(parent, HOOK, w, { name: "useChatSession", fold: "open", tint: ORIGIN.own });
const ctx = card(parent, HOOK, w, { name: "useChatSession", fold: "open", tint: ORIGIN.own, calls: 1 });
nameProp(ctx, "useChatSession", C.fn, [EXPORTED]);
prop(ctx, "参数", px("1 个", C.desc), { mono: false, fold: "open" });
table(ctx, [{ name: "名称", color: C.variable }, { name: "类型", color: C.type }], [["id", cx("字符串")]]);
@@ -807,7 +818,7 @@ function hookCard(parent, w) {
table(ctx, [{ name: "名称", color: C.variable }, { name: "是什么", mono: false }], [["messages", [link("状态 messages", HOOK.color)]]]);
}
// ─── 钩子调用:基础卡片加扩展行 ──────────────────────────────────────────────
// ─── 钩子调用:一种卡片 ──────────────────────────────────────────────────────
// 所有钩子卡片左边第一个图标、描边都一样;淡底按钩子从哪来分:React 自带、第三方库、项目里自己写的
const HOOK = { label: "钩子", color: "#b180d7", icon: "fishing-hook" };
@@ -824,8 +835,8 @@ const ROLE = {
concurrent: { label: "并发", icon: "hourglass", color: "#ce9178" },
};
// 所有钩子卡片同一个标签列宽,最长的标签是上下文的「没有外层提供时」
const HOOK_LABEL_W = 100;
// 所有钩子卡片同一个标签列宽,和最长的标签「类型参数」对齐
const HOOK_LABEL_W = 76;
/** 依赖项的三种读法:某个值变化时、空数组的仅挂载时、没写依赖的每次渲染后。 */
const changed = (name) => [seg(name, C.variable), seg(" 变化时", C.keyword)];
@@ -855,33 +866,20 @@ function fnBox(params, count) {
};
}
/** 一段代码后面跟一句灰字(「1 秒后」「在过渡里」)。 */
function codeWithNote(code, note) {
return cx(code).concat(note ? [seg(" " + note, C.desc)] : []);
}
/**
* 一处或几处「位置 | 写法」的表(在哪改、在哪派发、用在、调用位置):位置都能点过去,写法照译。
* `rows` 每项是 `[位置, 写法, 灰字?]`。
*/
function placesRow(ctx, label, rows) {
prop(ctx, label, px(`${rows.length} 个`, C.desc), { mono: false, fold: "open" });
table(ctx, [{ name: "位置" }, { name: "写法" }], rows.map(([where, how, note]) => [[link(where, HOOK.color)], codeWithNote(how, note)]));
}
/**
* 钩子调用的卡片,所有钩子共用一种:钩子 / 类型参数 / 参数 / 返回四行照钩子签名排,没有的行不显示,
* `extend` 在后面追加这类钩子自己的行。左边第一个图标、描边所有钩子一样,淡底按来源取:
* React 自带的(给 `role`)左边再并排一个作用图标;第三方库的(`lib`)、自己写的(`own`)只有钩子图标,自己写的钩子名能点到定义。
* `roleTip` 换掉作用图标的悬停文字,`bubble` 画出悬停时的提示气泡。
* `fold` 表示有函数参数,函数体子块收起。`params` 每项是「参数 | 值」,`returns` 每项是「名称 | 是什么」,格子同 [table]。
* 钩子调用的卡片,所有钩子共用一种,只有钩子 / 类型参数 / 参数 / 返回四行,照钩子签名排,没有的行不显示。
* 左边第一个图标、描边所有钩子一样,淡底按来源取:React 自带的(给 `role`)左边再并排一个作用图标;
* 第三方库的(`lib`)、自己写的(`own`)只有钩子图标,自己写的钩子名能点到定义。
* `roleTip` 换掉作用图标的悬停文字,`bubble` 画出悬停时的提示气泡。`fold` 表示有函数参数,函数体子块收起。
* `params` 每项是「参数 | 值」;`returns` 每项是「名称 | 是什么」,给了 `namesOnly` 只列名称。格子同 [table]。
* 返回值里有函数时给 `calls`:右上角多一个调用树按钮,数字是这个函数在样例里被调用几次。
*/
function hookCall(parent, w, h) {
const tint = h.own ? ORIGIN.own : h.lib ? ORIGIN.library : ORIGIN.react;
const icons = [{ name: HOOK.icon, color: HOOK.color, tip: HOOK.label }];
if (h.role)
icons.push({ name: h.role.icon, color: h.role.color, tip: h.roleTip || h.role.label, bubble: h.bubble });
const ctx = card(parent, HOOK, w, { name: h.name + " · " + h.target, fold: h.fold ? "closed" : null, labelW: HOOK_LABEL_W, tint, icons });
const ctx = card(parent, HOOK, w, { name: h.name + " · " + h.target, fold: h.fold ? "closed" : null, labelW: HOOK_LABEL_W, tint, icons, calls: h.calls });
const line = row(ctx.content, "Prop / 钩子", { gap: 12, align: "start" });
const key = row(line, "Key", { width: ctx.labelW, height: 20 });
text(key, "钩子", { size: 12.5, color: C.desc, lh: 20 });
@@ -894,37 +892,15 @@ function hookCall(parent, w, h) {
}
if (h.returns) {
prop(ctx, "返回", px(`${h.returns.length} 个`, C.desc), { mono: false, fold: "open" });
table(ctx, [{ name: "名称", color: C.variable }, { name: "是什么", mono: false }], h.returns);
if (h.namesOnly)
table(ctx, [{ name: "名称", color: C.variable }], h.returns.map((name) => [name]));
else
table(ctx, [{ name: "名称", color: C.variable }, { name: "是什么", mono: false }], h.returns);
}
if (h.extend)
h.extend(ctx);
}
/** 副作用的扩展行:改动(「改了 | 写法」表,没改动不显示)、清理函数(有才写)。 */
function effectRows(o) {
return (ctx) => {
if (o.changes) {
prop(ctx, "改动", px(`${o.changes.length} 个`, C.desc), { mono: false, fold: "open" });
table(ctx, [{ name: "改了" }, { name: "写法" }], o.changes.map(([what, how]) => [what, cx(how)]));
}
if (o.cleanup)
prop(ctx, "清理函数", cx(o.cleanup));
};
}
/** 改了什么:状态、引用前面写上是哪种。 */
const stateOf = (name) => [seg("状态 ", C.fg), seg(name, C.variable)];
const refOf = (name) => [seg("引用 ", C.fg), seg(name, C.variable)];
// ─── 钩子调用:样例里的每一处 ────────────────────────────────────────────────
// 别的卡片点到副作用时用的名字
const SESSION_EFFECT = "副作用「id 变化时」";
const OPEN_EFFECT = "副作用「open 变化时」";
const MOUNT_EFFECT = "副作用「仅挂载时」";
const SET_TIMER = "timer.current = window.setTimeout(…)";
const PUSH_MESSAGE = "setMessages(m => [...m, e.data])";
/** 第 14 行:自定义钩子里的 `useState`,作用图标画出悬停气泡。 */
function messagesStateHook(parent, w) {
hookCall(parent, w, {
@@ -935,7 +911,7 @@ function messagesStateHook(parent, w) {
typeArg: "Message[]",
params: [["initialState", cx("空数组")]],
returns: [["messages", "当前值"], [px("setMessages", C.fn), "修改函数"]],
extend: (ctx) => placesRow(ctx, "在哪改", [[SESSION_EFFECT, PUSH_MESSAGE, "ws.onmessage 里"]]),
calls: 1,
});
}
@@ -947,7 +923,6 @@ function sessionEffectHook(parent, w) {
target: "id 变化时",
fold: true,
params: [["effect", fnBox(null, 3)], ["deps", changed("id")]],
extend: effectRows({ changes: [[stateOf("messages"), PUSH_MESSAGE]], cleanup: "ws.close()" }),
});
}
@@ -967,7 +942,6 @@ function themeHook(parent, w) {
target: "theme",
params: [["context", [link("ThemeContext", K.context.color)]]],
returns: [["theme", "上下文的值"]],
extend: (ctx) => prop(ctx, "没有外层提供时", px("'light'", C.string)),
});
}
@@ -977,7 +951,8 @@ function sessionHook(parent, w) {
name: "useChatSession",
target: "messages",
params: [["id", cx("id")]],
returns: [["messages", [link("状态 messages", HOOK.color)]]],
returns: ["messages"],
namesOnly: true,
});
}
@@ -988,7 +963,7 @@ function openStateHook(parent, w) {
target: "open",
params: [["initialState", [seg("首次渲染时调用 ", C.fg)].concat(cx("readOpen(id)"))]],
returns: [["open", "当前值"], [px("setOpen", C.fn), "修改函数"]],
extend: (ctx) => placesRow(ctx, "在哪改", [["toggle", "setOpen(next)"], [OPEN_EFFECT, "setOpen(假)", "1 秒后"]]),
calls: 2,
});
}
@@ -999,14 +974,7 @@ function reducerHook(parent, w) {
target: "state",
params: [["reducer", [link("reducer", C.fn)]], ["initialState", cx("{ count: 0 }")]],
returns: [["state", "当前值"], [px("dispatch", C.fn), "派发函数"]],
extend: (ctx) => {
prop(ctx, "动作", "", { fold: "open" });
table(ctx, [{ name: "动作", color: C.string }, { name: "状态怎么变" }], [
["'add'", [seg("count", C.variable), seg(" 加 ", C.fg), seg("1", C.number)]],
[px("其他", C.desc), px("不变", C.fg)],
]);
placesRow(ctx, "在哪派发", [["toggle", "dispatch({ type: 'add' })"]]);
},
calls: 1,
});
}
@@ -1016,6 +984,7 @@ function transitionHook(parent, w) {
name: "useTransition",
target: "isPending",
returns: [["isPending", "是否正在过渡"], [px("startTransition", C.fn), "启动过渡的函数"]],
calls: 1,
});
}
@@ -1027,10 +996,6 @@ function boxRefHook(parent, w) {
typeArg: "HTMLDivElement",
params: [["initialValue", cx("空")]],
returns: [["boxRef", "引用对象"]],
extend: (ctx) => {
prop(ctx, "指向", [seg("<div>", C.nativeTag), seg(" 元素", C.fg)]);
placesRow(ctx, "用在", [["<div>", "ref={boxRef}"], [MOUNT_EFFECT, "boxRef.current?.scrollTo(0, 0)"]]);
},
});
}
@@ -1042,7 +1007,6 @@ function timerHook(parent, w) {
typeArg: "数值",
params: [["initialValue", [seg("没写,", C.desc), seg("未定义", C.number)]]],
returns: [["timer", "引用对象"]],
extend: (ctx) => placesRow(ctx, "用在", [[OPEN_EFFECT, SET_TIMER], [OPEN_EFFECT + "的清理函数", "clearTimeout(timer.current)"]]),
});
}
@@ -1054,10 +1018,6 @@ function totalHook(parent, w) {
fold: true,
params: [["factory", fnBox(null, 2)], ["deps", changed("messages")]],
returns: [["total", "算出的值"]],
extend: (ctx) => prop(ctx, "算出", (frame, vw) => {
text(frame, cx("sum * 2"), { mono: true, maxW: vw });
text(frame, "其中 sum = messages.length", { mono: true, size: 12.5, color: C.desc, maxW: vw });
}),
});
}
@@ -1069,7 +1029,7 @@ function toggleHook(parent, w) {
fold: true,
params: [["callback", fnBox([seg("next", C.variable), seg(": ", C.fg), seg("布尔", C.type)], 2)], ["deps", ON_MOUNT]],
returns: [[px("toggle", C.fn), "记住的函数"]],
extend: (ctx) => placesRow(ctx, "调用位置", [["handleClick", "toggle(open 不成立)", "在过渡里"]]),
calls: 1,
});
}
@@ -1080,10 +1040,6 @@ function openEffectHook(parent, w) {
target: "open 变化时",
fold: true,
params: [["effect", fnBox(null, 2)], ["deps", changed("open")]],
extend: effectRows({
changes: [[stateOf("open"), "setOpen(假)"], [refOf("timer"), SET_TIMER]],
cleanup: "clearTimeout(timer.current)",
}),
});
}
@@ -1094,7 +1050,6 @@ function renderEffectHook(parent, w) {
target: "每次渲染后",
fold: true,
params: [["effect", fnBox(null, 1)], ["deps", NO_DEPS]],
extend: effectRows({ changes: [[cx("document.title"), "String(total)"]] }),
});
}
File diff suppressed because it is too large. Load diff