DrillLab
八股题库Question bank

105 道问答题,一道一卡105 questions, one card each

默认只显示问题 —— 先自己在心里答一遍,再展开对答案。答不上来就标「不会」,下次抽认卡会先抽它。Only the question shows by default. Answer it in your head first, then open the answer. Mark the ones you miss; the flashcard round puts those first.

0 / 105道自评过self-assessed
0Got it0模糊Shaky0不会No idea105还没做Not seen
正在读这台浏览器里的标记…Reading your marks from this browser…
标记不是打分,是给下一轮排队Marks are a queue, not a score

标「不会」的题会排到抽认卡最前面,标「会」的进低频池排最后。所以别客气 —— 觉得答得磕磕巴巴就标「模糊」。准备好了就去抽认卡“No idea” jumps to the front of the next round; “got it” drops to the back. So be honest — if it came out shaky, mark it shaky. Then go run a flashcard round.

题目Questions

筛出 36 道(共 105 道) · 第 3 / 3 页。36 of 105 questions · page 3 / 3.
React 与生态React & ecosystem#344

React 18 有哪些新变化

What are the new changes in react 18

看答案Show answer

一句话:核心是并发渲染(concurrent rendering)—— React 可以中断、暂停、恢复一次渲染, 好让高优先级的更新先跑。

五个具体变化(挑三四个说清就够):

  • 自动批处理(automatic batching)—— React 17 只在 React 事件回调里批处理,setTimeout、 Promise、原生事件里的多次setState 会各触发一次渲染。18 里全都批处理了。这是最容易被观察到的变化。
  • useTransition—— 把一个更新标记为「不着急」。 典型场景:输入框旁边有个很重的搜索结果列表,输入保持流畅,列表慢慢跟上
  • useDeferredValue—— 同一个目的, 但是从「值」的角度:给我一个滞后版本的值。
  • 新的 root API——createRoot 取代ReactDOM.render不换它就用不上任何并发特性
  • Suspense 支持 SSR—— 流式渲染、选择性 hydration。
    另外 useId(服务端客户端一致的 id)、useSyncExternalStore(给状态库用的)。

会追问:「StrictMode 在 18 里有什么变化?」——会额外「挂载 → 卸载 → 再挂载」一次, 所以 effect 的清理函数会被执行。这是为了提前暴露「没写清理函数」的组件(见 #332),为将来的 Offscreen 特性做准备。
「升级要注意什么?」—— 换 createRoot; 自动批处理可能让依赖「每次 setState 都立刻渲染」 的老代码行为改变 (必要时用 flushSync 逃出批处理)。

In one line: the core is concurrent rendering — React can interrupt, pause and resume a render so a higher-priority update goes first.

Five concrete changes — covering three or four properly is enough:

  • Automatic batching — React 17 only batched inside React event handlers, so several setState calls in a setTimeout, a Promise or a native event each triggered their own render. 18 batches all of them. This is the change you notice most easily.
  • useTransition — mark an update as “not urgent”. The classic case is an input next to a heavy list of search results: typing stays smooth and the list catches up.
  • useDeferredValue — same goal, from the value side: give me a version of this value that lags behind.
  • The new root APIcreateRoot replaces ReactDOM.render, and until you switch you get none of the concurrent features.
  • Suspense works on the server — streaming and selective hydration.
    Plus useId (ids that match between server and client) and useSyncExternalStore (for state libraries).

Follow-up: “What changed for StrictMode in 18?” — it now does an extra mount, unmount, mount, so effect cleanup functions actually run. The point is to expose components that forgot to write cleanup (see #332) and to prepare for the future Offscreen feature.
“What do you watch out for when upgrading?” — switch to createRoot; automatic batching can change the behaviour of old code that assumed “every setState renders right away” (use flushSync to escape a batch when you truly need to).

JSX两个最能感知的变化示意Illustrative
1// React 17:这里会渲染两次
2setTimeout(() => {
3 setA(1);
4 setB(2);
5}, 0);
6// React 18:只渲染一次(自动批处理)
7
8// useTransition:输入不卡,列表慢慢跟
9const [isPending, startTransition] = useTransition();
10
11function onChange(e) {
12 setQuery(e.target.value); // 紧急:输入框马上响应
13 startTransition(() => setList(filter(e.target.value))); // 不急
14}
15{isPending && <span>更新中…</span>}
1// React 17: this renders twice
2setTimeout(() => {
3 setA(1);
4 setB(2);
5}, 0);
6// React 18: renders once (automatic batching)
7
8// useTransition: the input stays responsive and the list catches up
9const [isPending, startTransition] = useTransition();
10
11function onChange(e) {
12 setQuery(e.target.value); // urgent: the input responds at once
13 startTransition(() => setList(filter(e.target.value))); // not urgent
14}
15{isPending && <span>Updating</span>}
React 与生态React & ecosystem#347

React.lazy 是干什么的

What is React lazy function

看答案Show answer

一句话:让组件按需加载—— 打包时被切成单独的 chunk, 真正渲染到它的时候才下载。 必须配 Suspense 给个占位。

为什么需要:SPA 默认把全站打成一个包, 首屏要下载所有页面的代码 (见 #321)。按路由切分是收益最大的一刀 —— 用户打开首页不该下载「设置页」的代码。

四个注意点:

  • lazy 的参数必须返回一个default 导出的 Promise —— 所以配的是 default export; 具名导出要自己包一层。
  • 动态 import()的路径不能是完全动态的变量, 打包工具需要在编译期能分析出来。
  • 加载失败要有兜底—— 网络断了 chunk 拉不下来, 要用错误边界包住(见 #333)。这一点很多人不提,是加分项。
  • 别切太碎 —— 每个 chunk 都是一次请求。

会追问:「怎么避免切换页面时闪一下 loading?」——预加载:鼠标悬停在链接上时 就调一次那个 import()(模块会被缓存)。 或者用 React 18 的useTransition让旧页面留在屏幕上直到新页面就绪。

In one line: it makes a component load on demand — the bundler cuts it into its own chunk, and the chunk is fetched only when you actually render it. It needs a Suspense around it to supply a placeholder.

Why you need it: by default an SPA is one bundle, so the first screen downloads the code for every page (see #321). Splitting per route is the highest-value cut — opening the home page should not download the settings page.

Four things to watch:

  • What you pass to lazy must return a Promise with a default export — so it pairs with default export; a named export needs a small wrapper.
  • The path in a dynamic import() cannot be a fully dynamic variable — the bundler has to be able to analyse it at build time.
  • Handle the failure case — if the network drops the chunk never arrives, so wrap it in an error boundary (see #333). Most people leave this out, so it is a bonus point.
  • Do not split too finely — every chunk is another request.

Follow-up: “How do you avoid a loading flash when switching pages?” — preload: call that import() once when the mouse hovers the link (the module gets cached). Or use React 18’s useTransition to keep the old page on screen until the new one is ready.

JSXlazy 的完整用法(含失败兜底)示意Illustrative
1const Settings = lazy(() => import("./pages/Settings"));
2
3<ErrorBoundary fallback={<p>加载失败,请刷新</p>}>
4 <Suspense fallback={<Spinner />}>
5 <Settings />
6 </Suspense>
7</ErrorBoundary>
8
9// 预加载:悬停时就开始下载
10const preload = () => import("./pages/Settings");
11<Link to="/settings" onMouseEnter={preload}>设置</Link>
1const Settings = lazy(() => import("./pages/Settings"));
2
3<ErrorBoundary fallback={<p>Loading failed, please refresh</p>}>
4 <Suspense fallback={<Spinner />}>
5 <Settings />
6 </Suspense>
7</ErrorBoundary>
8
9// Preloading: start downloading on hover
10const preload = () => import("./pages/Settings");
11<Link to="/settings" onMouseEnter={preload}>Settings</Link>
React 与生态React & ecosystem#332

什么是 StrictMode

What is React strict mode

看答案Show answer

一句话:一个只在开发模式生效的检查工具组件, 通过故意多做一次来暴露不安全的写法。生产构建里它什么都不做。

它做三件事:

  • 渲染函数调用两次—— 暴露渲染过程中的副作用 (改了外部变量、直接 mutate props)。因为 render 阶段可能被中断重跑 (见 #353),所以它必须是纯的。
  • React 18 起:effect 会 「挂载 → 卸载 → 再挂载」——专门暴露「没写清理函数」的 effect。 如果你的组件挂两次就出问题 (重复订阅、定时器翻倍), 那就是真 bug。
  • 警告废弃 API 和过时的 ref 写法。

最重要的一句: 「双重渲染导致的问题」不是 StrictMode 的问题, 是你的代码的问题。很多人第一反应是「关掉它」—— 正确反应是修好副作用。

会追问:「日志被打印两次正常吗?」—— 开发模式下正常,因为渲染函数被调了两次; 生产不会。
「请求被发两次呢?」——这个要认真看: 说明你的 effect 没有正确处理清理 —— 虽然对幂等的 GET 无害, 但它同时提示你「竞态防护写了没有」。我们那道 fetch 变式题里ignore 标志 +abort 的写法, 正好让它在 StrictMode 下也表现正确。

In one line: a checking component that only does anything in development, and it exposes unsafe code by deliberately doing things twice. In a production build it does nothing.

It does three things:

  • Calls your render function twice — which surfaces side effects during render (mutating an outer variable, mutating props directly). Because the render phase can be interrupted and re-run (see #353), it has to be pure.
  • Since React 18: effects run mount, unmount, mountspecifically to expose effects with no cleanup function. If mounting twice breaks your component (a duplicated subscription, a doubled timer), that is a real bug.
  • Warns about deprecated APIs and legacy ref patterns.

The most important sentence: a problem caused by double rendering is not StrictMode’s problem, it is your code’s. Most people’s first instinct is to turn it off — the right instinct is to fix the side effect.

Follow-up: “Is it normal for my log to print twice?” — yes in development, because the render function ran twice; it will not in production.
“What about my request firing twice?” — take that one seriously: it means your effect is not cleaning up properly. Harmless for an idempotent GET, but it is also telling you to ask whether you guarded against races. The ignore flag plus abort pattern from our fetch variant is exactly what makes it behave correctly under StrictMode too.

React 与生态React & ecosystem#333

什么是错误边界,有什么用

What are error boundaries and How are they useful

看答案Show answer

一句话:一个能捕获子树渲染错误的类组件, 出错时渲染兜底 UI,而不是让整个应用白屏

怎么写:实现 getDerivedStateFromError(用来切换到兜底 UI)和componentDidCatch(用来上报日志)。目前必须是 class 组件—— 没有对应的 hook。

抓不到什么(这才是考点):

  • 事件处理函数里的错误—— 因为那不在渲染过程中。 要自己 try/catch
  • 异步代码里的错误——setTimeout、 Promise 回调(和 #310 同一个道理)。
  • 服务端渲染
  • 错误边界自己抛的错。

放哪:粒度要合适。只在根节点放一个, 一个小组件挂了整页还是没了;推荐按「独立区块」放—— 每个路由页面、 每个可独立失败的挂件 (侧栏、图表、评论区)。 这样一块坏了其他还能用。

会追问:「怎么让用户能恢复?」—— 兜底 UI 里给一个「重试」按钮, 把边界的 state 重置 (或者换一个 key强制重建子树)。react-error-boundary这个库提供了 resetErrorBoundary

In one line: a class component that catches render errors in its subtree and renders a fallback UI instead of letting the whole app go blank.

How you write one: implement getDerivedStateFromError (to switch to the fallback) and componentDidCatch (to report the error). It still has to be a class component — there is no hook equivalent.

What it does not catch — this is the real question:

  • Errors inside event handlers — those do not happen during render. Use your own try/catch.
  • Errors in async codesetTimeout, Promise callbacks (the same reason as #310).
  • Server-side rendering.
  • Errors thrown by the boundary itself.

Where to put them: get the granularity right. One at the root only means a single broken widget still wipes out the page; put one around each independent block — each route, and each widget that can fail on its own (sidebar, chart, comments). Then one broken block leaves the rest usable.

Follow-up: “How does the user recover?” — give the fallback a “Retry” button that resets the boundary’s state (or changes a key to force the subtree to rebuild). The react-error-boundary library gives you resetErrorBoundary for this.

JSX错误边界示意Illustrative
1class ErrorBoundary extends React.Component {
2 state = { error: null };
3
4 static getDerivedStateFromError(error) {
5 return { error }; // 切到兜底 UI
6 }
7
8 componentDidCatch(error, info) {
9 report(error, info.componentStack); // 上报
10 }
11
12 render() {
13 if (this.state.error) {
14 return (
15 <div>
16 <p>这块出错了</p>
17 <button onClick={() => this.setState({ error: null })}>重试</button>
18 </div>
19 );
20 }
21 return this.props.children;
22 }
23}
24
25// 按区块放,而不是只在根节点放一个
26<ErrorBoundary><Sidebar /></ErrorBoundary>
27<ErrorBoundary><Chart /></ErrorBoundary>
1class ErrorBoundary extends React.Component {
2 state = { error: null };
3
4 static getDerivedStateFromError(error) {
5 return { error }; // switch to the fallback UI
6 }
7
8 componentDidCatch(error, info) {
9 report(error, info.componentStack); // report it
10 }
11
12 render() {
13 if (this.state.error) {
14 return (
15 <div>
16 <p>Something went wrong in this part</p>
17 <button onClick={() => this.setState({ error: null })}>Retry</button>
18 </div>
19 );
20 }
21 return this.props.children;
22 }
23}
24
25// Put one per region rather than a single one at the root
26<ErrorBoundary><Sidebar /></ErrorBoundary>
27<ErrorBoundary><Chart /></ErrorBoundary>
React 与生态React & ecosystem#334

React Router 的意义是什么

React router, What is the point of it

看答案Show answer

一句话:让 SPA有「URL」这个概念—— 把地址栏和当前渲染哪个组件对应起来, 不刷新页面就能切换。

它解决四件事:

  • URL ↔ 组件的映射
  • 前进后退能用—— 接管 history API
  • 深链接可分享—— 发给别人能直接打开那个页面
  • 嵌套路由—— 布局层和内容层分开 (Outlet

核心 API:BrowserRouterRoutes / RouteLink /NavLinkuseNavigateuseParamsuseSearchParamsOutlet

三个实践要点:

  • Link 不要用<a>—— 后者会真的刷新整页, SPA 的意义就没了。
  • BrowserRouter 需要服务端 把所有路径都回退到index.html, 否则刷新子路由会 404。 没法配服务器就用HashRouter这题很常问。
  • React.lazy按路由切代码(#347)。

会追问:「路由守卫怎么做?」—— React Router 没有内置守卫, 自己写一个包装组件: 没登录就 <Navigate to="/login" />

In one line: it gives an SPA the concept of a URL — it maps the address bar to whichever component is rendered, and switches between them without a page reload.

It solves four things:

  • Mapping URLs to components
  • Back and forward work — it takes over the history API
  • Deep links are shareable — send one to someone and it opens that page directly
  • Nested routes — the layout layer and the content layer stay separate (Outlet)

The core API: BrowserRouter, Routes / Route, Link / NavLink, useNavigate, useParams, useSearchParams, Outlet.

Three practical points:

  • Use Link, not <a> — an anchor really does reload the whole page, which throws away the point of an SPA.
  • BrowserRouter needs the server to fall back to index.html for every path, otherwise refreshing a nested route 404s. If you cannot configure the server, use HashRouter. This one comes up a lot.
  • Pair it with React.lazy to split code per route (#347).

Follow-up: “How do you do route guards?” — React Router has none built in; you write a wrapper component yourself: if the user is not signed in, render <Navigate to="/login" />.

React 与生态React & ecosystem#348

写 React 时你会注意哪些最佳实践

When coding React, what are some best practices that you keep in mind

看答案Show answer

这是开放题,答「有取舍的清单」比列一堆规则好。按重要性给六条:

  • 不可变更新。永远造新对象/新数组, 不就地改 —— 否则 React 比引用发现不了变化,界面不更新。这是 React 里最常见的 bug 来源。
  • 能算出来的别放 state。派生数据当场算, 不要用 useEffect 同步两份数据 —— 同一个事实存两份必然会不一致。
  • state 放在「刚好够用」的那一层。提太高造成 drilling 和多余渲染, 提太低兄弟拿不到。
  • effect 里建立的东西一定要清理。定时器、监听器、订阅、在途请求。判别法:effect 里出现setInterval /addEventListener /subscribe /fetch,就一定要return
  • 列表 key 用稳定业务 id,不用 index。
  • 先测量再优化。别默认给所有东西套useMemo

再补两条工程上的:组件保持小而专一 (一个组件干一件事);用 TypeScript—— props 的形状是组件的契约, 写下来比靠记忆可靠。

如果面试官想听更具体的, 可以说: 「我会开着 eslint-plugin-react-hooks不用注释关掉exhaustive-deps 警告—— 它几乎每次都是对的, 想绕过它通常说明该重构了。」 这条很能体现实战经验。

This is an open question, and a list with trade-offs beats a pile of rules. Six, most important first:

  • Update immutably. Always build a new object or array, never edit in place — otherwise React compares references, sees nothing changed, and the UI does not update. This is the single most common source of bugs in React.
  • If you can compute it, do not store it in state. Derive it on the spot; do not use useEffect to keep two copies in sync — one fact stored twice will drift.
  • Keep state at the lowest level that works. Too high and you get prop drilling and wasted renders; too low and siblings cannot reach it.
  • Anything an effect sets up has to be torn down. Timers, listeners, subscriptions, in-flight requests. The test: if the effect contains setInterval, addEventListener, subscribe or fetch, it must return something.
  • Use stable business ids as list keys, not the index.
  • Measure before optimising. Do not wrap everything in useMemo by default.

Two more on the engineering side: keep components small and single-purpose (one component, one job); and use TypeScript — the shape of the props is the component’s contract, and writing it down beats remembering it.

If the interviewer wants something more specific, you can say: “I keep eslint-plugin-react-hooks on and I do not silence the exhaustive-deps warning with a comment — it is right almost every time, and wanting to get around it usually means the code needs restructuring.” That one really shows hands-on experience.

React 与生态React & ecosystem#349

Redux vs Context API

Redux vs Context API

看答案Show answer

一句话(这句最关键):它们解决的不是同一个问题。Context 是「传递」方案 (怎么把值送到深处), Redux 是「状态管理」方案 (状态怎么组织、怎么改、怎么调试)。

「Context 能不能替代 Redux」—— 严格说,Context + useReducer可以覆盖 Redux 的基本功能, 但缺三样东西:

  • 没有精细的订阅。context 一变,所有消费者都重渲染, 哪怕它只用了其中一个字段。 Redux 的 useSelector只在你选的那部分变化时重渲染。这是最大的实际差别。
  • 没有中间件。统一处理异步、日志、 持久化都要自己造。
  • 没有 DevTools。时间旅行、action 记录、 状态 diff 都没有。
ContextRedux
适合主题、当前用户、语言 ——不常变频繁变、多处读写、需要调试追溯
订阅粒度整个 value按 selector
额外依赖
样板代码有(Redux Toolkit 后少很多)

会追问:「现在还用 Redux 吗?」—— 答得诚实一点:纯客户端状态很多项目改用 Zustand / Jotai(更轻);服务端数据用 TanStack Query / SWR(缓存、去重、重试是它们的本职, Redux 做这个是硬凑)。Redux 现在的强项是「复杂的、 有大量交互逻辑的客户端状态 + 要可追溯调试」, 而且一定要用 Redux Toolkit 而不是手写。

In one line — and this is the key sentence: they do not solve the same problem. Context is a delivery mechanism (how a value reaches something deep in the tree); Redux is a state management solution (how state is organised, changed and debugged).

“Can Context replace Redux?” — strictly speaking, Context plus useReducer covers Redux’s basic features, but three things are missing:

  • No fine-grained subscription. When the context changes, every consumer re-renders, even one that only reads a single field. Redux’s useSelector re-renders only when the slice you selected changes. That is the biggest practical difference.
  • No middleware. Handling async, logging and persistence in one place is all yours to build.
  • No DevTools. No time travel, no action log, no state diff.
ContextRedux
Good forTheme, current user, locale — rarely changesChanges often, read and written in many places, needs a debuggable trail
Subscription granularityThe whole valuePer selector
Extra dependencyNoneYes
BoilerplateLittleSome (far less since Redux Toolkit)

Follow-up: “Do people still use Redux?” — answer honestly: plenty of projects moved pure client state to Zustand or Jotai (lighter); server data goes to TanStack Query or SWR (caching, deduping and retries are their day job, and Redux doing it is a stretch). Redux’s remaining strength is complex client state with a lot of interaction logic that you need to be able to trace, and you should always use Redux Toolkit rather than write it by hand.

React 与生态React & ecosystem#350

Redux 的结构和工作流

Redux structure and workflow

看答案Show answer

一句话:单向环—— view 派发 action → 中间件处理 → reducer 算出新 state → store 更新 → 订阅的组件重渲染。

五个角色:

  • store——唯一的状态容器, 提供 getState /dispatch /subscribe
  • action——描述「发生了什么」的普通对象,必须有 type它只描述,不做事。
  • reducer——(state, action) => newState必须是纯函数
  • middleware—— 在 action 到 reducer 之前拦一道(#354)。
  • selector—— 从 store 里挑出组件需要的那部分。

Redux Toolkit(RTK)改变了什么—— 必答,因为现在没人手写 Redux 了:

  • createSlice一次生成 reducer + action creators + action types, 样板代码少一大半。
  • 内置 Immer, 所以你可以写起来像在改state.list.push(x), 实际产出的是新对象—— 但要注意这只在createSlice 里成立。
  • 默认装好 thunk 和 DevTools。
  • createAsyncThunk管异步的三个状态 (pending / fulfilled / rejected)。

会追问:「为什么必须单向?」—— 因为状态变化的路径唯一, 所以出 bug 时可以从 action 记录里 倒推每一步。 双向绑定的框架里 「这个值到底是谁改的」经常查不清。

In one line: a one-way loop — the view dispatches an action, middleware handles it, a reducer computes the new state, the store updates, and the subscribed components re-render.

Five roles:

  • store — the single container for state, exposing getState, dispatch and subscribe.
  • action — a plain object that describes what happened and must have a type. It only describes; it does nothing.
  • reducer (state, action) => newState, and it has to be pure.
  • middleware — intercepts the action on its way to the reducer (#354).
  • selector — picks the part of the store a component needs.

What Redux Toolkit (RTK) changed — you have to cover this, because nobody hand-writes Redux any more:

  • createSlice generates the reducer, the action creators and the action types at once, cutting more than half the boilerplate.
  • Immer is built in, so you can write what looks like a mutation — state.list.push(x) — and still get a new object out. Just remember this only holds inside createSlice.
  • thunk and DevTools are wired up by default.
  • createAsyncThunk handles the three async states (pending / fulfilled / rejected).

Follow-up: “Why does it have to be one-way?” — because there is exactly one path a change can take, so when something breaks you can walk back through the action log step by step. In a two-way binding framework, “who actually changed this value” is often unanswerable.

JavaScript现在真正会写的 Redux示意Illustrative
1// RTK 的 slice:reducer + actions 一次生成
2const todos = createSlice({
3 name: "todos",
4 initialState: [],
5 reducers: {
6 add(state, action) {
7 state.push(action.payload); // 看着是 mutate,Immer 会产出新 state
8 },
9 toggle(state, action) {
10 const t = state.find((x) => x.id === action.payload);
11 if (t) t.done = !t.done;
12 },
13 },
14});
15
16export const { add, toggle } = todos.actions;
17
18// 组件里
19const list = useSelector((s) => s.todos); // 只订阅这一部分
20const dispatch = useDispatch();
21dispatch(add({ id: Date.now(), text, done: false }));
1// An RTK slice generates the reducer and the actions together
2const todos = createSlice({
3 name: "todos",
4 initialState: [],
5 reducers: {
6 add(state, action) {
7 state.push(action.payload); // it looks like a mutation; Immer produces a new state
8 },
9 toggle(state, action) {
10 const t = state.find((x) => x.id === action.payload);
11 if (t) t.done = !t.done;
12 },
13 },
14});
15
16export const { add, toggle } = todos.actions;
17
18// In the component
19const list = useSelector((s) => s.todos); // subscribes to this part only
20const dispatch = useDispatch();
21dispatch(add({ id: Date.now(), text, done: false }));
React 与生态React & ecosystem#352

Redux 的三大原则

Redux 3 main principles

看答案Show answer

三条,每条都要说出「为什么」:

  1. 单一数据源(single source of truth)—— 整个应用一个 store。
    为什么:状态只有一份就不会不一致, 而且整个应用的状态可以被序列化—— 这才有了「保存/恢复现场」和 SSR 脱水注水。
  2. state 只读—— 只能通过派发 action 改。
    为什么:把「谁能改状态」收窄到一个入口, 于是所有变化都可以被记录、 可以被拦截、可以被回放。
  3. 用纯函数(reducer)做修改——(state, action) => newState
    为什么:纯函数给定同样的 state 和 action 永远得到同样结果, 所以能重放、能测试、时间旅行调试才成立

这三条是一个整体: 单一数据源让状态可序列化, 只读让变化可记录, 纯 reducer 让变化可重放 ——三者合起来才有 DevTools 的时间旅行。能把它们串起来讲比一条条背强得多。

会追问:「reducer 里能做什么不能做什么?」——不能:改传进来的 state、 发请求、读Date.now() /Math.random()、 派发别的 action。这些都放中间件或 action creator 里。
「RTK 里 state.push()违反第 2 条吗?」—— 不违反。Immer 给的是一个草稿代理(draft proxy), 你的修改被记录下来, 最终产出的是新对象,原 state 没动。

Three of them, and each one needs a “why”:

  1. Single source of truth — one store for the whole app.
    Why: one copy of the state cannot disagree with itself, and the entire app state can be serialised — which is what makes “save and restore the session” and SSR dehydration possible.
  2. State is read-only — you change it only by dispatching an action.
    Why: it narrows “who can change state” down to one entrance, so every change can be logged, intercepted and replayed.
  3. Changes are made by pure functions (reducers) (state, action) => newState.
    Why: a pure function always gives the same result for the same state and action, so it can be replayed and tested, and that is what makes time-travel debugging work.

The three are one package: a single source of truth makes state serialisable, read-only makes changes loggable, and pure reducers make changes replayable — together they add up to time travel in DevTools. Tying them together like this is much stronger than reciting them one by one.

Follow-up: “What can and cannot a reducer do?” — it cannot mutate the state it was given, make requests, read Date.now() or Math.random(), or dispatch other actions. All of that belongs in middleware or an action creator.
“Does state.push() in RTK break rule 2?” — no. Immer hands you a draft proxy, records your edits, and produces a new object; the original state is untouched.

React 与生态React & ecosystem#354

解释一下 Redux 中间件

explain Redux Middleware

看答案Show answer

一句话:中间件是夹在「派发 action」和 「reducer 收到 action」之间的一层, 能拦截、改写、延迟、 甚至吞掉一个 action。

签名是三层柯里化—— 这个形状本身常被问到:store => next => action => {}。 多个中间件靠 next串成一条链,和 Express 的中间件是同一个模式

为什么需要它:因为reducer 必须是纯的, 所以异步和副作用无处安放。 中间件就是专门给副作用留的位置

常见的几个:

  • redux-thunk—— 让你能 dispatch 一个函数而不只是对象, 在里面做异步。最简单,RTK 默认装。
  • redux-saga—— 用 generator 描述复杂异步流程 (可取消、可重试、能编排多个请求)。 能力强但学习成本高。
  • redux-logger—— 打印每个 action 前后的 state。

会追问:「thunk 和 saga 怎么选?」—— 大部分项目 thunk 够了;只有在需要「取消、去抖、 复杂的流程编排」时 saga 才值那份复杂度
「能自己写一个吗?」—— 能,而且面试常让手写一个 logger。

In one line: middleware is a layer between “an action is dispatched” and “the reducer receives it”. It can intercept, rewrite, delay or even swallow an action.

The signature is curried three levels deep — the shape itself gets asked about: store => next => action => {}. Several middlewares chain together through next, and it is the same pattern as Express middleware.

Why you need it: because the reducer has to be pure, async work and side effects have nowhere to live. Middleware is the place reserved for side effects.

The common ones:

  • redux-thunk — lets you dispatch a function instead of only an object, and do your async work inside it. Simplest option, and RTK installs it by default.
  • redux-saga — describes complex async flows with generators (cancellable, retryable, able to orchestrate several requests). Powerful, but a steep learning curve.
  • redux-logger — prints the state before and after each action.

Follow-up: “thunk or saga?” — thunk is enough for most projects; saga only earns its complexity when you need cancellation, debouncing or real flow orchestration.
“Could you write one?” — yes, and interviewers often ask you to write a logger on the spot.

JavaScript中间件的形状与 thunk示意Illustrative
1// 手写一个 logger 中间件:注意那三层箭头
2const logger = (store) => (next) => (action) => {
3 console.log("派发:", action.type, action.payload);
4 const result = next(action); // 交给下一个中间件 / reducer
5 console.log("新状态:", store.getState());
6 return result;
7};
8
9// thunk 让 dispatch 能收函数
10const fetchUser = (id) => async (dispatch) => {
11 dispatch({ type: "user/loading" });
12 try {
13 const res = await fetch(`/api/users/${id}`);
14 if (!res.ok) throw new Error(`HTTP ${res.status}`); // 别忘了这一句
15 dispatch({ type: "user/loaded", payload: await res.json() });
16 } catch (e) {
17 dispatch({ type: "user/failed", payload: e.message });
18 }
19};
1// Writing a logger middleware yourself: note the three levels of arrows
2const logger = (store) => (next) => (action) => {
3 console.log("dispatching:", action.type, action.payload);
4 const result = next(action); // hand it to the next middleware or the reducer
5 console.log("new state:", store.getState());
6 return result;
7};
8
9// thunk lets dispatch accept a function
10const fetchUser = (id) => async (dispatch) => {
11 dispatch({ type: "user/loading" });
12 try {
13 const res = await fetch(`/api/users/${id}`);
14 if (!res.ok) throw new Error(`HTTP ${res.status}`); // do not forget this line
15 dispatch({ type: "user/loaded", payload: await res.json() });
16 } catch (e) {
17 dispatch({ type: "user/failed", payload: e.message });
18 }
19};
React 与生态React & ecosystem#355

JavaScript vs TypeScript

Javascript vs TypeScript

看答案Show answer

一句话:TS 是 JS 的超集—— 加了静态类型,编译后就是普通 JS, 运行时没有任何 TS 的东西

JavaScriptTypeScript
类型检查运行时才炸编译期就报
需要构建不需要需要(tsc / esbuild / SWC)
IDE 支持靠猜精确补全、跳转、重命名
重构靠搜字符串改一处,所有不兼容的地方都报出来

「运行时没有 TS」这句要强调, 因为它推出两个重要结论:

  • 类型不能用来做运行时校验。接口返回的数据是不是真的符合你写的interface, TS 管不了—— 要校验得用 zod 这类库。这是新手最大的误解。
  • as 断言只是「我保证」, 不做任何检查。滥用 asany 等于关掉了 TS。

代价(要主动说):多一步构建、 有学习成本(泛型、 条件类型、unknown vs any)、 第三方库缺类型时要自己写声明、 复杂类型报错很难读。

会追问:interfacetype 选哪个?」——interface能被重复声明合并、 更适合描述对象和 class 契约;type 能写联合、 交叉、映射、条件类型,能力更全。 实践上「对象形状用 interface, 其他用 type」,但团队统一比选哪个更重要

In one line: TS is a superset of JS — it adds static types, and after compilation it is ordinary JS with nothing of TS left at runtime.

JavaScriptTypeScript
Type checkingFails at runtimeReported at compile time
Build stepNot neededNeeded (tsc / esbuild / SWC)
IDE supportGuessworkPrecise completion, go-to-definition, rename
RefactoringSearch for stringsChange one place and every incompatible use lights up

Stress the “nothing of TS at runtime” line, because two important conclusions follow from it:

  • Types cannot validate anything at runtime. Whether the data an API returns really matches the interface you wrote is beyond TS — for that you need something like zod. This is the biggest beginner misconception.
  • An as assertion is just “trust me” and checks nothing. Overusing as and any is the same as turning TS off.

The costs — bring them up yourself: an extra build step, a learning curve (generics, conditional types, unknown vs any), writing your own declarations when a library ships none, and error messages for complex types that are hard to read.

Follow-up:interface or type?” — interface can be declared again and merged and suits object and class contracts; type can do unions, intersections, mapped and conditional types, so it is more capable. In practice: “interface for object shapes, type for everything else” — but a consistent team choice matters more than which one you pick.

React 与生态React & ecosystem#356

什么是静态类型检查,有什么好处

What is static type checking and how can developers benefit from it

看答案Show answer

一句话:不运行代码,只靠分析源码就找出类型不匹配的地方。 「静态」的意思就是「在编译期,而非运行期」。

四个具体收益(要给例子,别空谈):

  • 错误提前——user.nmae 拼错、 忘了处理 null、 给函数传少了参数,在编辑器里就红了, 而不是上线后用户报给你
  • 类型即文档—— 函数签名说明了它要什么、给什么。而且这份文档不会过期, 因为改了代码不改类型就编译不过。
  • 重构有底气—— 改一个字段名, 所有受影响的地方都会报错。这是 TS 最被低估的价值, 在大项目里比「防 bug」更实用。
  • IDE 能力—— 精确补全、跳定义、 安全重命名。

局限(说出来才显得懂):它只保证「类型对」,不保证「逻辑对」—— 类型全过的代码照样能算错工资。 而且它管不到运行时的外部数据(见 #355),所以类型检查不能替代测试

会追问:strict 模式开不开?」——新项目一定开。 最有价值的是strictNullChecks—— 它把「忘了判空」这一整类 运行时错误变成编译错误。
顺带一个真实例子:React 那门课的源项目npm run build 就是因为tsc 报了 10 个错误而失败的 (测试文件缺 vitest 全局类型)—— 这说明类型检查是构建的一部分, 不是可选的 lint

In one line: without running the code, purely by analysing the source, it finds places where the types do not line up. “Static” just means “at compile time, not at run time”.

Four concrete benefits — give examples, do not speak in the abstract:

  • Errors surface earlier — a typo like user.nmae, a forgotten null case, a missing argument: they go red in the editor instead of arriving as a user report after release.
  • Types are documentation — a signature says what it wants and what it gives back. And this documentation cannot go stale, because changing the code without changing the types fails the build.
  • Refactoring with confidence — rename one field and every affected place errors. This is TS’s most underrated value, and on a large codebase it is more useful than bug prevention.
  • Editor power — accurate completion, jump to definition, safe rename.

The limits — saying them is what shows you get it: it only guarantees the types are right, not that the logic is — fully typed code can still calculate the wrong salary. And it has no reach over external data at runtime (see #355), so type checking is not a substitute for tests.

Follow-up: “Do you turn on strict?” — always on a new project. The most valuable piece is strictNullChecks — it turns an entire class of “forgot the null check” runtime errors into compile errors.
A real example to go with it: npm run build on the source project for the React course fails precisely because tsc reports 10 errors (the test file is missing the vitest globals) — which shows type checking is part of the build, not an optional lint.

这些题从哪来Where these come from

99 道来自面试题库 #269–#387;TypeScript 深度那 6 道是 DrillLab 自出的(senior 补强,卡片上有标注)。答案都是 DrillLab 写的,所以讲解里的代码块一律标「示意」。每道题都能点回它出处的那一节课。99 questions come from the interview bank (#269–#387); the 6 TypeScript deep-dive ones are DrillLab-made (marked on the card). All answers are written by DrillLab, so every code block here is labelled “demo”. Each card links back to the lesson it came from.