题目重点:初始5行、每次多5行、30行禁用,同一按钮切换愿望状态;真实DOM动作序列独立验证。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 训练输入30个唯一ID商品、名称为文本;调用方提供root div/countLabel/loadMoreButton,原OA DOM/selector合同仍待核。
- 初始5、load每次+5、最多30;每行独立愿望状态,加入显示消息/Remove,移除恢复。默认英文文案是训练选择,题目Added to wishlisht可能为拼写回忆差异,支持addedMessage配置。
- 样例为原生JS模块,不改网站UI。正式OA如果只编译TS入口须把逻辑放实际打包文件;本文不伪造原repo入口名。
解题路线
- 唯一source of truth为visible与selected ID集合;DOM只展示,不以button文本猜状态。两不同商品同名也不能用name当key,ID先规范为String并检唯一。
- 初始append5个div行,load只append新增行,不重建已显示节点,保留已选状态和focus/事件。count随visible更新,30时load disabled;不要异步错误时先把visible永久加了却没有行。
- 每行创建一个button,click只切selected成员、同一button文案与aria-pressed,status文本对应状态。不通过两个隐藏/显示button替代,因为source明确该做法未过其测试。
- root下结构按给定合同,不能看示例影片table就擅自额外加table;文本用textContent,商品名称中的<符号不成为HTML。官方测试未知时自己的合理结构仍不能声称完全等价原DOM。
- a11y用真实button和按下状态,可补加载/加入通知的live region,新增内容焦点保持在load或按明确交互策略移动;本文样例不覆盖完整无障碍验收。
- 独立jsdom测试100条×300动作历史,以另一个Set/visible模型核DOM数量、按钮数量/身份、状态文案、aria、30cap与文本安全。不是只调getState就称DOM验证。
手动走一遍
初始5行,第二行加入后load一次变10行,第二行仍是同一个Remove按钮;再移除恢复。重复load至30后disabled,多点不会超过30。30,000动作状态在jsdom独立核对通过,包括名字含<script>仍是普通文本。
JavaScript 参考实现
下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。
代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。
// Teaching DOM contract: caller supplies the div root, count label and load button.
// Exactly 30 unique-ID products, supplied strings, no framework or table wrapper.
export function mountWishlist({ root, countLabel, loadMoreButton, products,
addedMessage = "Added to wishlist" }) {
if (products.length !== 30 || new Set(products.map(p => String(p.id))).size !== 30) {
throw new Error("30 products with distinct IDs required");
}
root.replaceChildren();
const selected = new Set();
let visible = 0;
function addFive() {
const next = Math.min(visible + 5, 30);
for (let i = visible; i < next; i++) {
const product = products[i];
const id = String(product.id);
const row = root.ownerDocument.createElement("div");
row.dataset.productId = id;
const name = root.ownerDocument.createElement("span");
name.textContent = `${id} ${product.name}`;
const button = root.ownerDocument.createElement("button");
button.type = "button";
button.textContent = "Add to wishlist";
button.setAttribute("aria-pressed", "false");
const status = root.ownerDocument.createElement("span");
button.addEventListener("click", () => {
if (selected.has(id)) selected.delete(id);
else selected.add(id);
const added = selected.has(id);
button.textContent = added ? "Remove from wishlist" : "Add to wishlist";
button.setAttribute("aria-pressed", String(added));
status.textContent = added ? addedMessage : "";
});
row.append(name, button, status);
root.append(row);
}
visible = next;
countLabel.textContent = String(visible);
loadMoreButton.disabled = visible === 30;
}
loadMoreButton.addEventListener("click", addFive);
addFive();
return { getState: () => ({ visible, selected: new Set(selected) }) };
}
复杂度
每次load最多创建5行,单次toggle期望O(1)集合更新/常量DOM节点修改;总30行状态O(30)。若正式变无界列表,需要窗口化/分页与异步请求合同,不能拿固定30上限推无限可扩展。
易错点与追问
容易失分的边界
- 用文案推状态。
- 两个button切hidden。
- 每load重建全部丢状态/focus。
- div下擅加table。
- 只改JS文件实际入口却是TS。
- 商品名innerHTML注入。
继续深挖
- 异步load失败/双击怎样保持幂等?
- 多实例挂载如何避免重复listener?
- 键盘与screen reader怎样通知新增行?
- selected持久化后数据换版本如何恢复?
