DOM、模块与工具链七问7 questions on the DOM, modules and tooling
DOM 与 DOM 事件、事件委托、ES6 新特性、ES6 模块、npm、Webpack、fetch vs axios。The DOM and DOM events, event delegation, new ES6 features, ES6 modules, npm, Webpack, fetch vs axios.
这一页有什么On this page8
- 01 什么是 DOM,什么是 DOM 事件What is the DOM, and what is a DOM event?
- 02 事件传播 vs 事件委托Event propagation vs event delegation
- 03 ES6 有哪些新特性What features did ES6 add?
- 04 什么是 ES6 模块What are ES6 modules?
- 05 什么是 npmWhat is npm?
- 06 Webpack 是怎么工作的How does Webpack work?
- 07 fetch 和 axios 的区别What is the difference between fetch and axios?
- 迁移模式Transfer
- 说清 DOM 是什么、以及为什么频繁操作 DOM 慢Explain what the DOM is, and why touching it many times is slow
- 写出事件委托并说明它解决了哪两个问题Write event delegation, and say which two problems it solves
- 分清 CommonJS 和 ES 模块在时机与语法上的差别Tell CommonJS and ES modules apart, in timing and in syntax
- 说出 Webpack 的四个核心概念和构建流程Name Webpack's four core concepts and describe the build steps
事件委托是唯一有区分度的一道 —— 它连着 React 的事件机制。模块和 Webpack 属于工程题,答得出「为什么需要打包」比背配置项更重要。fetch vs axios 是很实用的一道,我们那道 fetch 变式题的第一个坑就在这里。Event delegation is the only question here that separates candidates, because it connects to the way React handles events. Modules and Webpack are engineering questions; answering why bundling is needed matters more than reciting config options. fetch vs axios is a practical one — the first trap in our fetch variant exercise comes from it.
什么是 DOM,什么是 DOM 事件What is the DOM, and what is a DOM event?
#288 What is the DOM and what is DOM event
一句话:DOM 是浏览器把 HTML 解析成的一棵对象树, 每个标签是一个节点; DOM 事件是这棵树上发生的事情 (点击、输入、加载完成), 你可以注册函数去响应。
关键概念要点清:DOM 不是 HTML 本身, 也不属于 JavaScript 语言—— 它是浏览器提供的 API(Web API)。 所以 Node 里没有 document。 这条和 #276 是一组。
为什么「操作 DOM 慢」:不是读写属性本身慢,而是它可能触发重排(reflow)和重绘(repaint)—— 浏览器要重新计算布局、重新画。 在循环里反复读 offsetHeight再改样式,会造成强制同步布局(layout thrashing), 这才是真正的性能杀手。
这直接解释了虚拟 DOM 的价值(见 #330): 它把多次操作合并成一次, 并且尽量只改变化的部分。
会追问:「事件对象上 target 和currentTarget 什么区别?」——target 是真正被点的那个元素,currentTarget 是当前监听器挂在哪个元素上。事件委托全靠这个区别, 下一题就是。
In one line: the DOM is the tree of objects the browser builds when it parses your HTML — one node per tag. A DOM event is something that happens on that tree (a click, some typing, a load finishing), and you register functions to respond to it.
Be precise about the key point: the DOM is not the HTML itself, and it is not part of the JavaScript language — it is an API the browser hands you (a Web API). That is why Node has no document. This one pairs with #276.
Why “touching the DOM is slow”: reading and writing a property is not the slow part. The cost is that it can trigger reflow and repaint — the browser has to recompute layout and paint again. Reading offsetHeight and then changing a style, over and over inside a loop, causes layout thrashing, and that is the real performance killer.
This is exactly what makes the virtual DOM worth something (see #330): it batches many operations into one and tries to touch only what changed.
Follow-up: “What is the difference between target and currentTarget on the event object?” — target is the element that was actually clicked, currentTarget is the element this listener is attached to. Event delegation rides entirely on that difference, which is the next question.
事件传播 vs 事件委托Event propagation vs event delegation
#289 Event propagation vs Event delegation
一句话:传播是浏览器的机制(捕获 → 目标 → 冒泡,见 #380);委托是我们利用这个机制的技巧—— 把监听器挂在父元素上, 通过 e.target 判断实际点了哪个子元素。
委托解决两个问题:
- 监听器数量—— 1000 行的表格挂 1000 个监听器, 内存和绑定开销都很可观;委托只要 1 个。
- 动态元素—— 后来才插进来的子元素自动就有了行为, 不用重新绑定。这一条往往更实用。
写法的关键是 e.target.closest()—— 因为用户可能点在按钮里的<span> 上, 直接比 e.target.matches() 会漏。
会追问(重点):「React 的事件是委托的吗?」——是,而且这是它的核心设计: React 把事件统一挂在根容器上 (React 17 之前挂在 document, 17 之后挂到 root 节点,这是为了支持一个页面里多个 React 版本共存), 然后用合成事件(SyntheticEvent)模拟一套跨浏览器一致的事件系统。
推论:所以在 React 里e.stopPropagation()拦得住 React 组件之间的传播, 但拦不住原生监听器—— 因为原生的已经先跑完了。 这个点答出来会很加分。
In one line: propagation is the browser’s mechanism (capture → target → bubble, see #380); delegation is the trick we play with it — put the listener on the parent and use e.target to work out which child was really clicked.
Delegation solves two problems:
- The number of listeners — a 1000-row table with 1000 listeners costs real memory and real binding time; delegation needs one.
- Dynamic elements — children inserted later already have the behaviour, with nothing to rebind. In practice this is often the bigger win.
The key to writing it is e.target.closest() — the user may have clicked a <span> inside the button, so a bare e.target.matches() misses it.
Follow-up, and this is the one that matters: “Are React events delegated?” — yes, and it is central to the design: React attaches events to the root container (to document before React 17, to the root node from 17 onwards, so that several React versions can coexist on one page), then wraps them in a SyntheticEvent to present one event system that behaves the same across browsers.
The consequence: inside React, e.stopPropagation() does stop propagation between React components, but it cannot stop a native listener — the native one already ran. Landing this point scores well.
ES6 有哪些新特性What features did ES6 add?
#301 Name the new ES6 features
一句话:2015 年那一版改动最大, 十来个东西今天天天在用。
按重要性排(面试挑五六个说清就够, 别背清单):
let/const—— 块作用域(#282)- 箭头函数—— 简写 + 词法
this(#285) - 模板字符串——
`${x}`,支持多行 - 解构——
const { a, b } = obj,React 里到处在用 - 展开 / 剩余——
...,不可变更新的基础 - 默认参数
- Promise—— 异步的转折点(#306)
class—— 原型的语法糖(#302)- ES 模块——
import/export(#308) Map/Set(#286、#287)、Symbol、for…of与迭代器、生成器
会追问:「ES6 之后还有什么好用的?」—— 这题答得出来会显得你在跟进:async/await(ES2017)、 可选链 ?. 和空值合并??(ES2020)、Object.entries(ES2017)、Array.flat(ES2019)、at(-1)(ES2022)、structuredClone。
可选链和 ??这两个尤其值得提, 因为它们直接减少了大量防御式代码。
In one line: the 2015 edition changed the most, and a dozen or so of its additions are things you use every single day.
Ordered by weight — pick five or six and explain them properly, do not recite the list:
let/const— block scope (#282)- Arrow functions — shorter, plus a lexical
this(#285) - Template literals —
`${x}`, and they span lines - Destructuring —
const { a, b } = obj, used everywhere in React - Spread and rest —
..., the basis of immutable updates - Default parameters
- Promise — the turning point for async (#306)
class— sugar over prototypes (#302)- ES modules —
import/export(#308) Map/Set(#286, #287),Symbol,for…ofwith iterators, generators
Follow-up: “What came after ES6 that you like?” — answering this makes you look like you keep up: async/await (ES2017), optional chaining ?. and nullish coalescing ?? (ES2020), Object.entries (ES2017), Array.flat (ES2019), at(-1) (ES2022), structuredClone.
Optional chaining and ?? are the two most worth naming, because they cut out a mountain of defensive code.
什么是 ES6 模块What are ES6 modules?
#308 What are ES6 modules
一句话:语言内置的模块系统 ——export 导出、import 导入, 每个文件一个作用域,在编译期就能确定依赖关系。
和 CommonJS(Node 的老方案)的差别—— 这才是考点:
| CommonJS | ES Module | |
|---|---|---|
| 语法 | require / module.exports | import / export |
| 时机 | 运行时加载 | 编译期确定依赖 |
| 能否动态路径 | 能(require(x)) | 静态 import 不能;要动态用import()(返回 Promise) |
| 导出的是 | 值的拷贝 | 活的绑定(原值变了这边也变) |
| Tree shaking | 不行 | 可以(因为静态可分析) |
| 顶层 await | 不支持 | 支持 |
「编译期确定依赖」为什么重要?因为打包工具能在不运行代码的情况下 知道谁用了什么, 于是能删掉没用到的导出(tree shaking)、 能做代码分割。这是 ESM 最大的实际价值, 不是语法更好看。
会追问:「具名导出和默认导出怎么选?」—— 默认导出对重命名没有约束 (import 时可以叫任何名字), 不利于搜索和自动补全;具名导出更利于 tree shaking 和重构。 实践上「一个文件一个主体」用 default, 工具函数集合用具名。
「Node 里怎么用 ESM?」——package.json 里"type": "module", 或者文件名用 .mjs。 这就是 Federation 那门课里 Jest 需要--experimental-vm-modules 的原因。
In one line: the module system built into the language — export to expose, import to pull in, one scope per file, and dependencies are known at compile time.
How it differs from CommonJS, Node’s older scheme — this is the part being tested:
| CommonJS | ES Module | |
|---|---|---|
| Syntax | require / module.exports | import / export |
| Timing | Loaded at runtime | Dependencies resolved at compile time |
| Dynamic paths | Yes (require(x)) | Not with a static import; for that use import(), which returns a Promise |
| What gets exported | A copy of the value | A live binding (the original changes, so does this one) |
| Tree shaking | No | Yes, because it is statically analysable |
| Top-level await | Not supported | Supported |
Why does “resolved at compile time” matter? Because a bundler can see who uses what without running the code, so it can drop exports nobody imported (tree shaking) and split code. That is ESM’s real practical value, not prettier syntax.
Follow-up: “Named exports or a default export?” — a default export puts no constraint on the name, since an importer can call it anything, which hurts search and autocomplete; named exports are friendlier to tree shaking and refactoring. In practice: default for “one file, one main thing”, named for a bag of utilities.
“How do you use ESM in Node?” — "type": "module" in package.json, or name the file .mjs. That is why Jest needs --experimental-vm-modules in the Federation course.
什么是 npmWhat is npm?
#312 What is npm
一句话:Node 的包管理器 + 全球最大的包仓库。 它负责装依赖、锁版本、跑脚本。
三件核心事:
- 装依赖—— 读
package.json的dependencies, 递归下载到node_modules。 - 锁版本——
package-lock.json记下每一个包的确切版本和哈希, 保证队友和 CI 装出来的一模一样。它必须提交到版本库。 - 跑脚本——
npm run dev, 而且会把node_modules/.bin加到 PATH,所以能直接写vitest而不用写全路径。
必答的两个区分:
dependenciesvsdevDependencies—— 前者是运行时需要的 (React),后者只在开发和构建时需要 (TypeScript、测试框架、打包工具)。 生产安装可以用npm ci --omit=dev跳过后者。npm installvsnpm ci——install会在需要时更新 lock 文件;ci严格按 lock 装, 对不上就直接报错。CI 里应该用ci。
会追问:「^1.2.3 和~1.2.3 什么区别?」——^ 允许小版本和补丁升级 (<2.0.0),~ 只允许补丁 (<1.3.0)。正是因为 ^ 的存在, lock 文件才必不可少—— 否则不同时间装出来的版本会不同。
In one line: Node’s package manager, plus the largest package registry in the world. It installs dependencies, pins versions and runs scripts.
Three core jobs:
- Install dependencies — read
dependenciesout ofpackage.jsonand download the whole tree intonode_modules. - Pin versions —
package-lock.jsonrecords the exact version and hash of every package, so a teammate and CI install precisely what you did. It has to be committed. - Run scripts —
npm run dev. It also putsnode_modules/.binon the PATH, which is why you can writevitestinstead of a full path.
Two distinctions you must be able to make:
dependenciesvsdevDependencies— the first is what you need at runtime (React), the second only while developing and building (TypeScript, the test runner, the bundler). A production install can skip the second withnpm ci --omit=dev.npm installvsnpm ci—installwill update the lock file when it has to;ciinstalls strictly from the lock and errors out if the two disagree. CI should useci.
Follow-up: “What is the difference between ^1.2.3 and ~1.2.3?” — ^ allows minor and patch upgrades (<2.0.0), ~ allows patches only (<1.3.0). It is precisely because ^ exists that the lock file is indispensable — otherwise installing on two different days gives you two different trees.
Webpack 是怎么工作的How does Webpack work?
#283 How does Webpack work
一句话:从入口出发递归分析所有 import, 建出一张依赖图, 路上用 loader 把各种文件转成 JS, 最后按规则打成若干个 bundle。
四个核心概念(必答):
- entry—— 从哪开始找依赖。
- output—— 打到哪里、叫什么名 (带
[contenthash]做缓存)。 - loader——把非 JS 转成 JS。
babel-loader转 JSX 和新语法、css-loader让 CSS 能被import。loader 是从右到左(从下到上)执行的, 这个细节常问。 - plugin—— 在构建生命周期的各个钩子上干活, 能力比 loader 大得多 (生成 HTML、抽 CSS、压缩、分析体积)。
为什么需要打包(这才是问题的本质):浏览器早期不支持模块、 不认识 JSX 和 TS、 请求数多会慢; 打包解决的是模块化、转译、 合并压缩、以及 tree shaking。
会追问:「Vite 为什么快?」—— 这题现在很常问。 开发时 Vite 不打包, 直接用浏览器原生 ESM 按需提供模块, 所以启动是常数时间, 不随项目变大而变慢; 依赖预构建用 esbuild(Go 写的,快一个量级); 生产才用 Rollup 打包。
Webpack 是「先全量打包再服务」, Vite 是「先服务再按需转换」—— 这一句就说到了根本区别。
In one line: start at the entry, walk every import recursively and build a dependency graph, push files through loaders to turn them into JS along the way, and emit a handful of bundles according to your rules.
Four core concepts you have to name:
- entry — where the dependency walk starts.
- output — where the bundles land and what they are called (with
[contenthash]for caching). - loader — turns non-JS into JS.
babel-loaderhandles JSX and new syntax,css-loadermakes CSS importable. Loaders run right to left (bottom to top) — that detail gets asked a lot. - plugin — hooks into the build lifecycle and can do far more than a loader (generate HTML, extract CSS, minify, analyse bundle size).
Why bundling exists at all — that is the real question: early browsers had no modules, they do not understand JSX or TS, and a lot of requests is slow. Bundling buys you modularity, transpilation, merging and minifying, and tree shaking.
Follow-up: “Why is Vite fast?” — asked constantly now. In development Vite does not bundle; it serves modules on demand over the browser’s native ESM, so startup is constant time and does not degrade as the project grows. Dependency pre-bundling runs on esbuild (written in Go, an order of magnitude faster), and only production goes through Rollup.
Webpack bundles everything and then serves it; Vite serves first and transforms on demand — that single sentence gets at the fundamental difference.
fetch 和 axios 的区别What is the difference between fetch and axios?
#387 What is the difference between making server requests via fetch and axios?
一句话:fetch 是浏览器内置的, 很基础;axios 是第三方库, 把常用的事都替你做了。
fetch | axios | |
|---|---|---|
| 依赖 | 无,浏览器/Node 18+ 内置 | 要装(约 13 KB) |
| 4xx / 5xx | 不 reject, 要自己查 res.ok | 自动抛错 |
| JSON | 要手动 await res.json() | 自动解析到 data |
| 超时 | 要自己配 AbortController | timeout 一个选项 |
| 拦截器 | 没有,要自己包一层 | 内置(统一加 token、统一处理 401) |
| 上传进度 | 很麻烦 | 支持 |
| Node 里可用 | 18+ 才有 | 一直可以 |
「404 不 reject」是这题的核心考点, 也是真实 bug 的来源:不查res.ok 就会把错误页当数据渲染。从 axios 转过来的人最容易漏这一条, 因为 axios 会自己抛。
怎么选:简单项目、在意包体积、 或者只发几个请求 → fetch包一个自己的小 wrapper; 需要拦截器 / 统一错误处理 / 上传进度,或者要兼容老 Node → axios。
更常见的现实答案:用TanStack Query / SWR管缓存和请求状态, 底下用哪个都行 —— 因为fetch 和 axios都不管缓存、去重、重试。 能这么答说明你想过分层。
In one line: fetch is built into the browser and very bare-bones; axios is a third-party library that does the routine work for you.
fetch | axios | |
|---|---|---|
| Dependency | None, built into browsers and Node 18+ | Must be installed (about 13 KB) |
| 4xx / 5xx | Does not reject; you check res.ok yourself | Throws for you |
| JSON | You call await res.json() by hand | Parsed into data already |
| Timeout | Wire up an AbortController yourself | One timeout option |
| Interceptors | None; wrap it yourself | Built in (attach a token everywhere, handle 401 in one place) |
| Upload progress | Painful | Supported |
| Available in Node | Only from 18 | Always has been |
“404 does not reject” is the heart of this question, and a real source of bugs: skip the res.ok check and you render an error page as if it were data. People coming over from axios miss this one most often, because axios throws on their behalf.
How to choose: a small project, a tight bundle budget, or only a handful of requests → fetch with your own small wrapper. Interceptors, one place for error handling, upload progress, or an old Node to support → axios.
The more realistic answer: reach for TanStack Query or SWR to manage caching and request state, and whichever one sits underneath hardly matters — because neither fetch nor axios handles caching, deduplication or retries. Answering that way shows you have thought about the layers.
换一道题也能用Works on other problems too
考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.
- DOM 是浏览器提供的对象树 API,不属于 JS 语言;慢是因为重排重绘,不是读写属性本身。The DOM is a tree of objects the browser provides; it is not part of the JavaScript language. It is slow because of reflow and repaint, not because reading or writing a property is slow.
- target 是被点的元素,currentTarget 是监听器挂在哪;事件委托全靠这个区别。target is the element that was clicked, currentTarget is where the listener is attached; event delegation depends on that difference.
- 委托解决监听器数量和动态元素两个问题;React 把事件委托到 root 并用合成事件。Delegation solves two problems, the number of listeners and elements added later; React delegates events to the root and uses synthetic events.
- ESM 编译期确定依赖 → 能 tree shaking;CommonJS 运行时加载、导出是值拷贝。ESM resolves its imports at compile time, which makes tree shaking possible; CommonJS loads at runtime and its exports are copies of the values.
- package-lock.json 必须提交;CI 用 npm ci 而不是 npm install。package-lock.json has to be committed; in CI use npm ci instead of npm install.
- Webpack 四概念 entry/output/loader/plugin,loader 从右到左执行。Webpack has four concepts, entry/output/loader/plugin, and loaders run from right to left.
- fetch 不因 4xx reject,必须查 res.ok —— 这是从 axios 转过来最容易漏的一条。fetch does not reject on a 4xx, so you have to check res.ok — the step most often forgotten when moving over from axios.