前端面试复习笔记:React 高频题 + JS 手写题
适合面试前快速过一遍。内容覆盖到 React 19(含 19.2 / 19.3 要点),按「能答清楚原理 + 能写出代码」整理。
目录
- [一、React 高频面试题](#一、React 高频面试题 "#%E4%B8%80react-%E9%AB%98%E9%A2%91%E9%9D%A2%E8%AF%95%E9%A2%98")
- [二、JavaScript 手写题](#二、JavaScript 手写题 "#%E4%BA%8Cjavascript-%E6%89%8B%E5%86%99%E9%A2%98")
一、React 高频面试题
1. React 18 / 19 有哪些重要更新?
React 18(仍常考的基础)
| 特性 | 一句话理解 |
|---|---|
| 并发渲染(Concurrent Rendering) | 更新可中断、可恢复,输入等高优任务不易被长任务卡住 |
| 新的根 API | ReactDOM.render → createRoot(...).render(...) |
| 自动批处理 | 事件、定时器、Promise 里的多次更新默认合并 |
| Suspense 增强 | 更好支持数据请求与 SSR 流式渲染 |
startTransition / useTransition |
标记非紧急更新,保持交互流畅 |
useDeferredValue |
推迟展示次要内容 |
useId |
生成 SSR/CSR 一致的唯一 ID |
js
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')).render(<App />);
React 19(面试重点,建议优先背)
React 19 在并发能力之上,把 Actions(异步更新链路) 、表单 、use API 、ref 用法 、文档元数据 / 资源预加载 ,以及 Server Components 推到更可用的位置。
| 特性 | 一句话理解 |
|---|---|
| Actions | 在 Transition 里跑异步逻辑,自动管 pending / 错误 / 乐观更新 |
useActionState |
专为 Action 设计的状态钩子(结果 + 提交函数 + pending) |
useFormStatus |
子组件读取父级 <form> 的提交状态 |
useOptimistic |
先更新 UI,请求失败再回滚 |
use |
在渲染中读取 Promise / Context(可配合 Suspense) |
ref 作为普通 prop |
函数组件可直接接收 ref,多数场景不再需要 forwardRef |
| ref 回调可返回 cleanup | 类似 useEffect 清理函数 |
<Context> 直接当 Provider |
可写 <ThemeContext value={...}>,不必再套 .Provider |
| Document Metadata | 组件里直接写 <title> / <meta> 等,React 会提升到 document.head |
| 样式表 / async script / preload | 内置更好的资源加载与去重 |
| Server Components / Server Actions | RSC 更稳定;服务端函数配合 "use server" |
Actions + 表单(最常考)
过去改名要自己管 isPending、错误、成功跳转。React 19 可用 Action 简化:
jsx
import { useActionState } from 'react';
function ChangeName() {
const [error, submitAction, isPending] = useActionState(
async (_prev, formData) => {
const err = await updateName(formData.get('name'));
return err || null;
},
null
);
return (
<form action={submitAction}>
<input name="name" />
<button disabled={isPending}>更新</button>
{error && <p>{error}</p>}
</form>
);
}
子组件读表单状态:
jsx
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? '提交中...' : '提交'}</button>;
}
乐观更新:
jsx
import { useOptimistic } from 'react';
function TodoList({ todos, sendTodo }) {
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, sending: true }]
);
// 发送前先 addOptimistic,失败时 React 会回到真实 todos
}
use API
jsx
import { use, Suspense } from 'react';
function Comments({ commentsPromise }) {
const comments = use(commentsPromise); // Promise pending 时触发最近的 Suspense
return comments.map((c) => <p key={c.id}>{c.text}</p>);
}
<Suspense fallback={<p>加载中...</p>}>
<Comments commentsPromise={fetchComments()} />
</Suspense>
注意:
use可在条件分支里调用(和普通 Hook 规则不完全一样)- 不要 在渲染里临时
use(new Promise(...))这种每次新建的 Promise - 也可
use(SomeContext)读取 Context
ref 作为 prop(React 19)
jsx
function Input({ placeholder, ref }) {
return <input placeholder={placeholder} ref={ref} />;
}
// 多数新代码不再需要 forwardRef
ref 回调还能返回清理函数:
jsx
<div
ref={(node) => {
// 挂载时拿到 node
return () => {
// 卸载 / ref 变化时清理
};
}}
/>
元数据与资源
jsx
function BlogPost({ post }) {
return (
<>
<title>{post.title}</title>
<meta name="author" content={post.author} />
<link rel="stylesheet" href="/blog.css" precedence="default" />
<article>{post.body}</article>
</>
);
}
也可用 preload / preinit 等 API 预加载字体、脚本。
Server Components(概念题)
- Server Components:默认在服务端跑,不把一大坨交互逻辑打进客户端包
- Client Components :文件顶部
"use client",才能用 state / 事件 - Server Actions :
"use server"标记的服务端函数,常和表单 Action 一起用
面试别只背名词:强调「减少客户端 JS、数据靠近服务端、表单/变更用 Action 串起来」。
React 19.2 / 19.3 可补充的加分点
| 版本 | 值得提的点 |
|---|---|
| 19.2 | <Activity>(隐藏/恢复 UI 与状态)、useEffectEvent(把"事件逻辑"从 Effect 依赖里拆出去)、部分预渲染 resume API、Performance Tracks |
| 19.3 | <ViewTransition> 稳定(进出场/移动动画)、Fragment refs 稳定、browser()(等浏览器环境再渲染)、Trusted Types、Server Components 可直接渲染 Context |
jsx
import { ViewTransition } from 'react';
<ViewTransition>
<Page />
</ViewTransition>
jsx
import { useEffectEvent, useEffect } from 'react';
function Chat({ roomId, onMessage }) {
const onMsg = useEffectEvent((msg) => onMessage(msg));
useEffect(() => {
const conn = connect(roomId);
conn.on('message', onMsg);
return () => conn.disconnect();
}, [roomId]); // 不必把 onMessage 塞进依赖
}
口述模板:
18 解决「怎么并发、怎么批处理」;19 解决「异步提交、表单、乐观更新、RSC 落地」;19.2/19.3 再补齐 Activity、Effect Event、View Transition 等体验与心智负担问题。
2. JSX 是什么?和 JS 有什么区别?
JSX 是 JavaScript 的语法扩展,让你用类似 HTML 的写法描述 UI。构建工具会把它编译成普通 JS(如 React.createElement 或新的 JSX 运行时)。
jsx
<div className="sidebar" />
大致等价于:
js
React.createElement('div', { className: 'sidebar' });
区别要点:
- JSX 不是合法原生 JS,需要编译
- 属性用
className、htmlFor,事件用驼峰(onClick) {}里写 JS 表达式
3. 简述 React 生命周期(类组件)
三大阶段:挂载 → 更新 → 卸载。
挂载
constructorstatic getDerivedStateFromPropsrendercomponentDidMount
更新(setState / props 变化 / forceUpdate)
static getDerivedStateFromPropsshouldComponentUpdaterendergetSnapshotBeforeUpdatecomponentDidUpdate
卸载
componentWillUnmount(清定时器、取消请求、解绑事件)
注意:
componentWillMount/componentWillReceiveProps/componentWillUpdate已过时,新项目别用。函数组件对应关系:
useEffect(() => {}, [])≈DidMount;带清理函数 ≈WillUnmount;依赖变化 ≈DidUpdate。
getDerivedStateFromProps 应用场景很窄。常见误用:无脑把 props 拷进 state。多数情况直接用 props,或在事件里更新 state 即可。
4. React 事件机制和原生 DOM 事件有什么区别?
| 原生 DOM | React 合成事件 | |
|---|---|---|
| 绑定位置 | 具体 DOM 节点 | React 17+ 绑在根容器 上(之前多在 document) |
| 事件对象 | 原生 Event |
SyntheticEvent(跨浏览器统一) |
| 执行顺序 | 捕获 → 目标 → 冒泡 | 同一轮里,原生监听通常先于 React 委托逻辑(仍建议少混用) |
React 用事件委托减少监听器数量,并统一行为。需要原生事件时可用 e.nativeEvent,或在 ref 上直接 addEventListener。
5. Redux 工作原理
核心:单一 store + 纯函数 reducer + 不可变更新。
流程:
- 组件
dispatch(action) - store 把当前 state 和 action 交给 reducer
- reducer 返回新 state
- 订阅者(如
useSelector/connect)感知变化并重渲染
原则:
- 唯一改状态的方式是 dispatch
- reducer 必须是纯函数
- 复杂异步常用中间件(redux-thunk / redux-saga)
现在很多项目也会用 Zustand / Jotai / React Query,但 Redux 的单向数据流思想仍常考。
6. React Router 工作原理?常用组件有哪些?
依赖 history(或自建 history)监听 URL 变化,再匹配路由表渲染对应组件。
常见 history 模式:
- BrowserHistory :真实路径,靠
pushState,需服务端兜底 - HashHistory :
#/path,兼容性好,不依赖服务端 rewrite - MemoryHistory:内存路由,适合 RN / 测试
常用组件(v6):
BrowserRouter/HashRouterRoutes/RouteLink/NavLinkOutlet(嵌套路由出口)Navigate、useNavigate、useParams、useSearchParams
7. Hooks 解决了什么问题?函数组件和类组件区别
Hooks 主要解决:
- 逻辑复用难:不用 HOC / render props 层层包裹
- 相关逻辑分散:同一业务散落在多个生命周期里
- 类组件心智负担 :
this、绑定、生命周期规则多
| 类组件 | 函数组件 + Hooks | |
|---|---|---|
| 状态 | this.state |
useState / useReducer |
| 副作用 | 生命周期方法 | useEffect / useLayoutEffect |
| 复用 | HOC、render props | 自定义 Hook |
| 代码形态 | 类、方法分散 | 按关注点组织 |
React 19 再补几个常考 Hook / API:useActionState、useOptimistic、useFormStatus、use,以及 19.2 的 useEffectEvent。
8. setState 是同步还是异步?它做了什么?
面试标准答法:
- 多数情况下表现为「异步」 :调用后立刻读
state往往还是旧值 - React 会把多次更新批处理合并,减少渲染次数
- React 18 起,在 Promise、
setTimeout等里也会自动批处理 - 需要基于最新值更新时用函数式写法:
js
setCount((c) => c + 1);
内部大致流程:合并更新 → 进入 reconciler 算新树 → Diff → 提交到真实 DOM。
9. 什么是 Fiber?解决了什么问题?
React 15 的 Stack Reconciler 用递归 Diff,无法中断。一旦计算过久,会卡住主线程(输入掉帧)。
Fiber 做了这些事:
- 把工作单元改成可中断的链表结构(
child/sibling/return) - 用循环替代不可打断的递归
- 结合优先级调度:紧急更新优先,低优更新可暂停
这是并发渲染、startTransition 等能力的基础。
10. React 中如何捕获渲染错误?
用 Error Boundary(错误边界),只能是类组件(或基于它的库封装):
jsx
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error(error, info);
}
render() {
if (this.state.hasError) return <h1>出错了</h1>;
return this.props.children;
}
}
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
捕获不到:
- 事件处理函数里的错误(用
try/catch) - 异步代码(
setTimeout、Promise)------若用 React 19 的use(promise)且错误在渲染链路中,可被边界/Suspense 相关机制处理;普通事件里的 Promise 仍要自己 catch - 服务端渲染错误
- 错误边界自身抛出的错误
11. 组件传值有哪些方式?
- 父 → 子:props
- 子 → 父:父传回调,子调用并带参
- 跨多层:Context
- 全局/复杂状态:Redux、Zustand 等
- URL 状态:路由 params / search
- 非响应式通信:EventBus(慎用,难追踪)
12. 无状态组件和类组件的区别
历史上「无状态」多指纯函数展示组件:只吃 props、无 state、无生命周期。
Hooks 出现后,函数组件也能有状态。现在更准确的对比是:
- 展示型:主要负责 UI
- 容器型:负责数据与副作用
函数组件通常更简洁,配合 React.memo、Hooks 也能做性能优化。
13. React 如何实现类似 Vue keep-alive 的缓存?
React 官方长期没有内置 keep-alive。常见做法:
- 第三方库 :如
react-activation、keepalive-for-react - 自己做缓存 :隐藏时用 CSS/
display保留 DOM,或把状态存到上层/全局 store - 路由级缓存:结合路由库做 outlet 缓存
- React 19.2+ :可关注官方
<Activity>------用于隐藏/恢复子树 UI 与内部状态(面试可提"方向上在补齐这类能力")
注意:缓存会占用内存,也要处理「返回页是否刷新数据」。
14. React 如何做路由监听?
React Router v6:
js
import { useLocation, useNavigationType } from 'react-router-dom';
import { useEffect } from 'react';
function RouteListener() {
const location = useLocation();
useEffect(() => {
console.log('当前路径', location.pathname);
}, [location]);
return null;
}
也可以监听原生:
js
window.addEventListener('popstate', handler);
window.addEventListener('hashchange', handler);
15. 有哪些方式会触发组件更新 / 改变 state?
setState/useState的 setteruseReducer的 dispatch- 父组件重渲染导致子组件 props 变化
- Context value 变化
- 类组件
forceUpdate(尽量少用) - 改变列表项
key强制重建
16. React 有哪几种创建组件的方式?
- 函数组件(主流)
- ES6 class 组件
(已淘汰)React.createClass
17. props 和 state 有什么区别?
| props | state | |
|---|---|---|
| 来源 | 外部传入 | 组件内部 |
| 可变性 | 组件内不应直接改 | 可通过 setter 更新 |
| 用途 | 配置、数据下发 | 交互产生的内部状态 |
相同 props + 相同 state,渲染结果应可预期(纯渲染思想)。
18. key 的作用是什么?
帮助 React 在列表 Diff 时识别「是同一个元素,还是该增删改」。
jsx
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
建议:
- 用稳定唯一 ID
- 不要用随机数当 key
- 列表会乱序/增删时,尽量别只用 index
19. ref 的作用是什么?
用于拿到 DOM 节点或类组件实例(少数场景)。
jsx
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}
原则:能用声明式就别用 ref。适合焦点、动画测量、与非 React 库集成。
React 19 变化(加分):
- 函数组件可直接把
ref当普通 prop 接收,新代码多数不必再写forwardRef - ref 回调可以
return清理函数 - 19.3 起 Fragment 也可挂 ref,方便操作一组 DOM 子节点
20. React Diff 的主要策略
- 同层比较:不跨层移动整棵树硬匹配
- 类型不同直接重建 :如
div换成p,整棵子树拆掉重建 - 列表靠 key:提高复用准确度
- 开发者可用
shouldComponentUpdate/React.memo跳过子树
目标不是保证绝对最优 Diff,而是在常见 UI 场景下足够快。
21. 受控组件和非受控组件
受控:表单值由 React state 驱动。
jsx
const [value, setValue] = useState('');
<input value={value} onChange={(e) => setValue(e.target.value)} />
非受控:值存在 DOM 里,通过 ref 读取。
jsx
const inputRef = useRef();
<input defaultValue="hello" ref={inputRef} />
多数表单推荐受控,便于校验与统一数据流;和原生插件集成时非受控更方便。
React 19 补充: <form action={fn}> / formAction 可直接接 Action。非受控字段在 Action 成功后会自动 reset;需要手动重置可用 requestFormReset。复杂校验仍常用受控,简单提交场景 Action 更省事。
22. 为什么说虚拟 DOM 能提升性能?
更准确的说法是:虚拟 DOM 提升的是「可维护性 + 更新可控性」,不是「一定比手写 DOM 更快」。
流程:
- 用 JS 对象描述 UI(Virtual DOM)
- 状态变了生成新树
- Diff 出补丁
- 批量更新真实 DOM
收益:
- 减少粗糙的整页重绘思维,尽量做最小更新
- 把 UI 更新变成数据驱动,好写也好推演
- 跨平台(RN 等)也可复用同一套思想
二、JavaScript 手写题
1. 数组去重
js
// Set
const unique1 = (arr) => [...new Set(arr)];
// filter
const unique2 = (arr) => arr.filter((item, i) => arr.indexOf(item) === i);
// reduce
const unique3 = (arr) =>
arr.reduce((acc, cur) => (acc.includes(cur) ? acc : [...acc, cur]), []);
对象/NaN 去重要另说;面试优先写 Set,再补充复杂度。
2. 数字千分位
js
const num = 1234567.89;
num.toLocaleString('en-US'); // "1,234,567.89"
// 正则版
function formatNumber(n) {
const [int, decimal] = String(n).split('.');
const formatted = int.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return decimal ? `${formatted}.${decimal}` : formatted;
}
3. 防抖与节流
js
function debounce(fn, wait) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait);
};
}
function throttle(fn, wait) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= wait) {
last = now;
fn.apply(this, args);
}
};
}
- 防抖:停下来才执行(搜索框)
- 节流:一段时间内最多一次(滚动)
4. 手写简化版 Promise
js
function MyPromise(executor) {
this.status = 'pending';
this.value = undefined;
this.reason = undefined;
this.onFulfilled = [];
this.onRejected = [];
const resolve = (value) => {
if (this.status !== 'pending') return;
this.status = 'fulfilled';
this.value = value;
this.onFulfilled.forEach((cb) => cb());
};
const reject = (reason) => {
if (this.status !== 'pending') return;
this.status = 'rejected';
this.reason = reason;
this.onRejected.forEach((cb) => cb());
};
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
MyPromise.prototype.then = function (onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const handle = (cb, data) => {
queueMicrotask(() => {
try {
const result = typeof cb === 'function' ? cb(data) : data;
resolve(result);
} catch (e) {
reject(e);
}
});
};
if (this.status === 'fulfilled') handle(onFulfilled, this.value);
else if (this.status === 'rejected') handle(onRejected, this.reason);
else {
this.onFulfilled.push(() => handle(onFulfilled, this.value));
this.onRejected.push(() => handle(onRejected, this.reason));
}
});
};
说明:这是面试简化版,未完整实现 Promise/A+(如 thenable 递归解析)。
5. 深浅拷贝
js
function shallowCopy(obj) {
if (obj === null || typeof obj !== 'object') return obj;
return Array.isArray(obj) ? [...obj] : { ...obj };
}
function deepCopy(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (map.has(obj)) return map.get(obj);
const clone = Array.isArray(obj) ? [] : {};
map.set(obj, clone);
for (const key of Reflect.ownKeys(obj)) {
clone[key] = deepCopy(obj[key], map);
}
return clone;
}
6. 手写 new
js
function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype);
const result = Constructor.apply(obj, args);
return result !== null && (typeof result === 'object' || typeof result === 'function')
? result
: obj;
}
7. 函数柯里化
js
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) return fn.apply(this, args);
return (...rest) => curried.apply(this, args.concat(rest));
};
}
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
curriedSum(1)(2)(3); // 6
curriedSum(1, 2)(3); // 6
8. Promise 封装 AJAX
js
function ajax({ url, method = 'GET', data = null, headers = {} }) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
Object.entries(headers).forEach(([k, v]) => xhr.setRequestHeader(k, v));
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) resolve(xhr.responseText);
else reject(new Error(xhr.statusText));
};
xhr.onerror = () => reject(new Error('Network Error'));
xhr.send(data);
});
}
9. 不借助临时变量交换 a、b
js
let a = 5;
let b = 10;
[a, b] = [b, a];
数字也可用加减/异或,但解构最清晰。
10. 数组求和
js
arr.reduce((sum, n) => sum + n, 0);
11. 数组扁平化
js
function flatten(arr) {
return arr.reduce(
(acc, cur) => acc.concat(Array.isArray(cur) ? flatten(cur) : cur),
[]
);
}
// 现代 API
nested.flat(Infinity);
12. 实现 add(1)(2)(3) 累加(可扩展)
js
function add(a) {
let sum = a;
function inner(b) {
sum += b;
return inner;
}
inner.valueOf = () => sum;
inner.toString = () => String(sum);
return inner;
}
console.log(+add(1)(2)(3)); // 6
13. 类数组转数组
js
Array.prototype.slice.call(arrayLike);
Array.from(arrayLike);
[...arrayLike]; // 需可迭代
注意:普通 {0:'a', length:1} 不能直接 [...],用 Array.from 更稳。
14. 扁平数据转树
js
function buildTree(items, parentId = null) {
return items
.filter((item) => item.parentId === parentId)
.map((item) => {
const children = buildTree(items, item.id);
return children.length ? { ...item, children } : { ...item };
});
}
15. 红灯 3s、绿灯 1s、黄灯 2s 循环
js
function light(color, ms) {
return new Promise((resolve) => {
console.log(color);
setTimeout(resolve, ms);
});
}
async function traffic() {
while (true) {
await light('红', 3000);
await light('绿', 1000);
await light('黄', 2000);
}
}
traffic();
16. Promise 异步加载图片
js
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
});
}
17. 发布订阅
js
class EventBus {
constructor() {
this.events = {};
}
on(type, cb) {
(this.events[type] || (this.events[type] = [])).push(cb);
}
off(type, cb) {
if (!this.events[type]) return;
this.events[type] = this.events[type].filter((fn) => fn !== cb);
}
emit(type, ...args) {
(this.events[type] || []).forEach((fn) => fn(...args));
}
}
取消订阅时必须传入同一个函数引用。
18. async/await 封装 fetch
js
async function fetchJSON(url, options) {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
19. 极简双向绑定思路(Object.defineProperty / Proxy)
面试能讲清链路即可:依赖收集 → 设置值通知 → 更新视图。
js
function bindInput(input, obj, key) {
input.value = obj[key];
Object.defineProperty(obj, key, {
configurable: true,
get() {
return input.value;
},
set(v) {
input.value = v;
},
});
input.addEventListener('input', (e) => {
obj[key] = e.target.value;
});
}
现代框架多用 Proxy;React 本身是单向数据流,表单「受控组件」是显式同步。
20. 简易 Hash 路由
js
const routes = {
'/': () => 'Home',
'/about': () => 'About',
};
function render() {
const path = window.location.hash.slice(1) || '/';
document.getElementById('app').textContent = (routes[path] || routes['/'])();
}
window.addEventListener('hashchange', render);
window.addEventListener('load', render);
21. 斐波那契
js
function fib(n) {
if (n <= 1) return n;
let a = 0;
let b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
}
递归能写,但要主动提时间复杂度与记忆化。
22. 最长无重复子串长度
js
function lengthOfLongestSubstring(s) {
const seen = new Set();
let left = 0;
let max = 0;
for (let right = 0; right < s.length; right++) {
while (seen.has(s[right])) {
seen.delete(s[left++]);
}
seen.add(s[right]);
max = Math.max(max, right - left + 1);
}
return max;
}
23. 用 setTimeout 模拟 setInterval
js
function mySetInterval(fn, delay) {
let timer = null;
let stopped = false;
const run = () => {
if (stopped) return;
fn();
timer = setTimeout(run, delay);
};
timer = setTimeout(run, delay);
return () => {
stopped = true;
clearTimeout(timer);
};
}
const cancel = mySetInterval(() => console.log('tick'), 1000);
// cancel();
优点:上一次执行完再预约下一次,比 setInterval 更不容易「重叠堆积」。
写在最后
如果你也在准备面试,建议按这个顺序刷:
- 先把 React 原理题讲顺(Fiber、diff、setState、Hooks)
- 再把 React 19 :Actions /
use/refas prop / RSC 讲清楚 - 每天手写 2~3 道 JS(防抖节流、深拷贝、Promise、扁平化、发布订阅)
- 每题准备「是什么 / 为什么 / 怎么用 / 坑点」四句话
有写错或想补充的题,欢迎评论交流。