面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月29日 22:35

Zustand 中如何处理异步操作?

Zustand 的 store 就是个普通对象,create 回调里直接写 async 函数即可,不需要 thunk 之类的中间件。写法:在 create((set, get) => ({ ... })) 里定义 async action,内部 await 拿到数据后调 set({ data, loading: false })。手动管理 loading/error 状态是最常见的方式。如果嫌重复,用 zustand/middleware 的 immer 简化嵌套更新,或封装一个 createAsyncAction 工具函数统一处理 loading/success/error 三态。追问Zustand 和 Redux Toolkit 处理异步有什么区别?RTK 需要 createAsyncThunk + extraReducers 处理三态,模板代码多。Zustand 直接在 action 里 await + set,没有中间件概念,代码量少一半以上。RTK 的优势是 DevTools 自动追踪异步状态,Zustand 需要手动 devtools middleware。并发请求怎么处理?两个独立请求各自 async action 并行调用即可。如果需要等全部完成:await Promise.all([fetchA(), fetchB()])。注意竞态问题——快速切换页面时旧请求后到会覆盖新数据,用请求 ID 或 AbortController 取消旧请求。Suspense 能配合 Zustand 用吗?可以,但需要包装成 throw promise 的模式:store 里存 promise 而非数据,组件读时如果 promise 未 resolve 就 throw 出去,Suspense 捕获。推荐用 use 包(React 19 内置)或 suspend-react 简化。不过大多数项目手动 loading 状态更直观,Suspense 方案适合设计系统级别统一处理。如何做请求缓存和去重?简单方案:store 里维护 Map<cacheKey, { data, timestamp }>,action 里先查缓存未过期就直接返回。复杂场景用 SWR 或 TanStack Query 管缓存,Zustand 只管 UI 状态,职责分离更清晰。服务端渲染(SSR)时异步怎么处理?create 时传入 hydrate 数据,客户端 useEffect 里发起请求覆盖。注意避免服务端和客户端数据不一致的 hydration mismatch——初始渲染用服务端数据,客户端请求完成后 set 更新即可。
服务端阅读 05月29日 22:35

Solidity 中 view、pure 和 payable 函数修饰符有什么区别?

view 可读不可写状态变量,pure 不可读也不可写,payable 允许接收 ETH。无修饰符的函数可读可写。view 和 pure 不消耗 gas(外部调用时),因为节点可以本地模拟执行而不上链。但 view/pure 在合约内部被交易调用时,调用者仍需付 gas。payable 的唯一作用是让函数能通过 msg.value 收到 ETH,非 payable 函数收到 ETH 会自动 revert。追问view 函数真的不花 gas 吗?外部调用(call / eth_call)不花 gas,因为是只读模拟。但如果一笔交易内部调用了 view 函数,那笔交易本身要付 gas——view 只是承诺不修改状态,不代表调用它的上下文免费。payable 和 non-payable 的 gas 差异?non-payable 函数开头会自动插入 require(msg.value == 0) 检查(约 200 gas)。payable 跳过这个检查,所以 gas 略低。如果函数明确需要收 ETH,加 payable 既是功能需求也省 gas。为什么编译器会警告"view 函数修改了状态"?因为你在 view 函数里调用了写操作(写 storage、发 ETH、触发事件等)。编译器按修饰符检查,不符合就报错。解决:要么去掉 view(确实需要写),要么确保只读。emit 事件在 view 函数中也不允许,因为事件本身是状态变更的日志。pure 函数里能用 block.timestamp 吗?不能。block.timestamp、block.number、msg.sender 都属于读取区块链状态,pure 里不允许。只允许用函数参数和内存变量做纯计算。如果你需要读链上状态但不写,用 view。接口中的 view/pure 声明有什么用?接口中声明 view/pure 是给编译器的契约——实现合约的对应函数也必须是 view/pure。如果实现合约把 view 改成 non-view,编译会报错。这保证了外部调用者可以安全地用 eth_call 调用而不用发交易。
服务端阅读 05月29日 22:35

Solidity 中 delegatecall 和 call 有什么区别?代理合约怎么实现?

call 在被调用合约的上下文执行,msg.sender 是调用者,存储读写被调用合约的 storage。delegatecall 在调用者的上下文执行,msg.sender 保持原始调用者不变,存储读写调用者的 storage——代码是别人的,存储是自己的。代理合约就是靠 delegatecall 实现的:代理合约存数据,逻辑合约存代码,fallback 函数 delegatecall 到逻辑合约,逻辑合约操作的是代理的 storage。追问透明代理和 UUPS 有什么区别?透明代理(Transparent Proxy):代理合约的 admin 调管理函数、user 调业务逻辑,在代理中用 if (msg.sender == admin) 分流,无函数选择器冲突风险但 gas 多约 2000。UUPS:升级逻辑写在逻辑合约里,代理更轻量,但逻辑合约忘了写 _authorizeUpgrade 就永远锁死——安全性依赖逻辑合约代码质量。存储碰撞怎么防?代理合约和逻辑合约的存储布局必须对齐——变量声明顺序一致,新版本只能追加变量不能删除或插入。用 OpenZeppelin 的 StorageSlot 或 EIP-1967 规定特定 slot 存代理管理数据(如 bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)),避免和业务变量撞 slot。代理合约升级时旧数据会丢吗?不会。数据存在代理合约的 storage 里,逻辑合约升级只是换了 delegatecall 的目标地址,代理的 storage 不变。但新逻辑合约的存储布局必须兼容旧布局——删除或重排变量会导致旧数据错位。为什么不直接用 call 替代 delegatecall?call 执行后状态变更在被调用合约上,调用者(代理)的 storage 不变——等于白调了。代理模式的核心就是"数据在代理、逻辑在实现",只有 delegatecall 能让实现合约操作代理的 storage。Beacon 代理是什么?多个代理合约共享同一个逻辑合约地址,升级时改一处全部生效。结构:Proxy → Beacon(存 implementation 地址)→ Logic Contract。好处是管理 100 个代理只需一次升级,gas 省。坏处是多一层间接调用。
服务端阅读 05月29日 22:35

Solidity 中如何安全地生成随机数?

Solidity 无法原生生成真随机数——block.timestamp、block.difficulty、blockhash 都可被矿工操纵,永远不要用于决定资金分配。安全方案分三类:Chainlink VRF(链上验证的链下随机数,生产首选)、commit-reveal(双方各提交哈希再揭示,适合两方博弈)、Randao/Drand(去中心化随机数网络,多节点协作生成)。Chainlink VRF 流程:请求时传 seed → Chainlink 链下生成随机数 + 证明 → 回调函数中验证证明后使用随机数,链上可验证不可篡改。追问blockhash 为什么不安全?blockhash(block.number - 1) 看似不可预测,但矿工可以选择性打包交易——如果随机结果对自己不利就不出块。而且 blockhash 只保留最近 256 个区块,超过就返回 0。Chainlink VRF 的 gas 消耗高吗?请求约 5 万 gas,回调验证约 15-20 万 gas(含证明验证)。总成本约 0.01-0.03 ETH。用 VRF v2 的订阅模式可以预充 Link,比直接支付更灵活。NFT mint、抽奖等高频场景需要考虑成本。commit-reveal 适合什么场景?两个参与者博弈(如石头剪刀布):先提交 keccak256(choice + secret) 的哈希(commit),双方都提交后再揭示原始值(reveal),验证哈希匹配。缺点是需要两轮交易,用户体验差,不适合多方或高频场景。如何防止前端运行随机数结果?即使随机数来源安全,攻击者可以在 mempool 中看到交易结果后决定是否抢跑。对策:用回调模式(结果在下一次交易中返回,而非同一交易)、加最小延迟、或使用 Flashbots 等私有内存池。游戏中随机数用什么方案?小额休闲游戏用 Chainlink VRF v2(成本可控、链上可验证)。大型链游用混合方案:VRF 生成种子 → 链上伪随机函数展开成序列 → 玩家行为(提交 nonce)参与混合,兼顾公平和性能。
服务端阅读 05月29日 22:35

Solidity 中 ECDSA 签名验证的原理是什么?如何实现?

ECDSA 签名验证就是用私钥签名、用公钥验证,链下签名链上验证。Solidity 用 ecrecover(hash, v, r, s) 从签名恢复出签名者地址,再对比是否为预期地址。标准流程:bytes32 hash = keccak256(abi.encodePacked(...)) → 用 EIP-712 结构化哈希 → 链下签名得 (r, s, v) → 链上 ecrecover 恢复地址。OpenZeppelin 的 ECDSA 库封装了边界检查和 malleability 防护。追问EIP-712 为什么比普通 keccak256 签名好?普通签名 keccak256(abi.encodePacked(...)) 用户看到的是一串十六进制,无法判断签了什么。EIP-712 定义了结构化的类型化数据,钱包(MetaMask)会显示人类可读的内容("你正在授权转移 100 USDC"),防止钓鱼签名。签名重放攻击怎么防?在签名数据中包含 nonce(递增计数器)和 address(this)(合约地址)。部署新合约后旧签名失效(地址变了),同一合约内每个 nonce 只能用一次。缺少 nonce 攻击者可以重复提交同一签名。ecrecover 返回 address(0) 意味着什么?签名无效时 ecrecover 不 revert,而是返回零地址。所以必须检查 recovered != address(0),否则攻击者构造一个恢复为零地址的签名就能绕过验证。OpenZeppelin 的 ECDSA.recover 已经内置了这个检查。签名延展性(Malleability)是什么?ECDSA 签名 (r, s) 中,s 可以替换为 n - s(n 是椭圆曲线阶数)得到另一个合法签名,签名不同但恢复的地址一样。EIP-2 要求 s 在曲线阶数的上半部分,ECDSA.toEthSignedMessageHash 和 OpenZeppelin 库都做了这个约束。多签钱包如何用 ECDSA 实现?链下收集足够多签名,链上逐一 ecrecover 验证,检查恢复出的地址都在授权列表中且不重复。Gnosis Safe 就是这个模式——不需要链上存储 nonce 状态,gas 更省,但需要链下协调签名顺序。
服务端阅读 05月29日 22:35

Solidity 智能合约有哪些常见安全漏洞?如何防止?

最致命的 5 类漏洞:重入攻击(Reentrancy)——用 Checks-Effects-Interactions 模式或 ReentrancyGuard;整数溢出——Solidity 0.8+ 内置检查,0.7 及以下用 SafeMath;权限控制缺失——关键函数加 onlyOwner / onlyRole,用 OpenZeppelin 的 AccessControl;闪电贷操纵价格——用 TWAP 而非现货价格,加交易延迟;前端运行(MEV)——用 commit-reveal 方案或私有内存池。核心原则:所有外部调用都是不安全的,所有用户输入都是恶意的。追问重入攻击为什么最难防?因为 transfer / call 会把控制权交给对方合约,对方可以回调你的函数,而你的状态还没更新。Checks-Effects-Interactions 模式强制先改状态再转账,ReentrancyGuard 用锁变量硬性阻止递归进入。两者都用最稳。0.8 之后真的不需要 SafeMath 了吗?算术运算溢出会自动 revert,是的。但类型转换溢出不检查——uint256 i = type(uint256).max; uint8 j = uint8(i) 会静默截断。unchecked 块内的运算也不检查,只在 gas 优化场景使用且确保不会溢出。如何防止闪电贷攻击?闪电贷让攻击者在单笔交易内借到巨量资金操纵价格后归还。防御:用 Uniswap V3 TWAP(时间加权平均价格)取代现货价格;限制单笔交易的滑点范围;加 block.timestamp 延迟阻止同区块操作。delegatecall 有什么安全隐患?delegatecall 在调用者上下文执行被调用者的代码——意味着被调用合约可以修改调用者的存储布局。如果 slot 对不上(存储碰撞),可能覆盖 owner 地址。代理合约模式必须严格对齐存储布局,用 OpenZeppelin 的透明代理或 UUPS 避免手动管理。审计工具能替代人工审计吗?不能。Slither / Mythril / Foundry Fuzz 能找到已知的模式型漏洞,但业务逻辑漏洞(如价格计算公式错误、奖励分配不公平)只能人工审查。工具 + 人工审计 + 测试网演练三者缺一不可。
服务端阅读 05月29日 22:35

Solidity 中如何处理时间锁(Timelock)机制?

时间锁就是给合约操作加一个延迟:提案创建后必须等待指定时间(如 48 小时)才能执行,期间可以取消。核心实现:mapping(bytes32 => uint256) public queuedTimestamp,queue() 记录时间戳,execute() 检查 block.timestamp >= queuedTimestamp[id] + delay。OpenZeppelin 的 TimelockController 是生产级实现,支持多角色( proposer / executor / admin)和最小延迟保障。追问Timelock 和 multisig 哪个更安全?不互斥,通常组合使用。Multisig 防止单点私钥风险,Timelock 防止即时作恶——即使 multisig 签了名,社区也有时间审查和反应。Uniswap、Compound 的治理都是 multisig + timelock 双层。如何防止 Timelock 被绕过?关键:delay 和 minDelay 只能通过 Timelock 自身的提案修改(self-governance),不能有外部 admin 直接改延迟。OpenZeppelin 的 TimelockController 默认就是这样——admin 角色也必须走提案流程。什么操作必须加时间锁?代币增发(mint)、升级代理合约(upgrade)、修改费率、提取资金——凡是影响用户资产的操作都该加。只读操作和紧急暂停(pause)通常不加,因为暂停是保护性操作。Timelock 的 gas 消耗如何?queue 和 execute 各约 5-8 万 gas,主要是 SSTORE 和权限检查。批量操作(batch / scheduleBatch)可以省一些,因为共享一次权限检查。如何实现可取消的时间锁?加 cancel(bytes32 id) 函数,只有 proposer 角色可以调用,删除 queuedTimestamp[id]。执行时如果找不到时间戳就 revert。争议操作被社区反对时,proposer 可以主动取消,避免硬分叉。
服务端阅读 05月29日 22:35

WebGL 缓冲区(Buffer)是什么?VBO 和 VAO 有什么区别?

WebGL 缓冲区就是 GPU 显存中的一块区域,用来存顶点数据(位置、颜色、法线、UV 等)。VBO(Vertex Buffer Object)是存数据的容器,VAO(Vertex Array Object)是记录"哪个 VBO 绑到哪个 attribute、偏移量多少、步长多少"的配置快照。WebGL 1 没有 VAO(需扩展 OES_vertex_array_object),WebGL 2 原生支持。有了 VAO,切换绘制对象只需 gl.bindVertexArray(vao) 一行,不用重复设置一堆 vertexAttribPointer。追问VAO 具体记录了哪些状态?每个 attribute 的启用状态(enableVertexAttribArray)、绑定的 VBO(vertexAttribPointer 时的 ARRAY_BUFFER)、数据偏移和步长、以及 ELEMENT_ARRAY_BUFFER 的绑定。不记录 ARRAY_BUFFER 本身的绑定——这点容易搞混。为什么 WebGL 1 没有 VAO?OpenGL ES 2.0 规范没包含 VAO,它从 OpenGL ES 3.0 / WebGL 2 才成为标准。WebGL 1 可以用扩展 OES_vertex_array_object,但不是所有设备都支持。项目兼容性要求高的话,自己封装一个 VAO 管理器,内部用数组存 attribute 配置,绑定时批量调用 vertexAttribPointer。EBO(IBO)和 VBO 什么关系?EBO(Element Buffer Object)也叫 IBO,存索引数据,告诉 GPU 按什么顺序读顶点,实现顶点复用(一个正方体 8 个顶点而非 36 个)。EBO 绑定到 ELEMENT_ARRAY_BUFFER,绘制时用 gl.drawElements 而非 gl.drawArrays。EBO 的绑定状态记录在当前 VAO 里。什么场景必须手动管理 Buffer?动态更新的数据(粒子系统、变形动画)需要 gl.bufferData 分配大小后用 gl.bufferSubData 局部更新,避免每帧重新分配显存。静态数据(模型网格)创建一次即可,设 gl.STATIC_DRAW 提示驱动放显存。多个 VBO 怎么组织到同一个 VAO?同一个 VAO 绑定期间,依次 bindBuffer + vertexAttribPointer 注册每个 VBO 到不同的 attribute location。也可以把所有数据交错打包到一个 VBO 里(interleaved),用 stride 和 offset 描述布局,减少 buffer 切换次数。
服务端阅读 05月29日 22:35

WebGL 雾效(Fog)是如何实现的?

WebGL 雾效的本质就是根据片段到相机的距离,在物体颜色和雾颜色之间做插值:finalColor = mix(fogColor, objectColor, fogFactor)。三种计算 fogFactor 的方式:线性雾 clamp((end - dist) / (end - start), 0, 1) 需要指定起止距离;指数雾 exp(-density * dist) 更自然,一个 density 参数搞定;指数平方雾 exp(-(density*dist)²) 过渡更柔和。深度值从视图空间的 -viewPos.z 或 length(viewPos.xyz) 获取,后者基于实际距离而非仅 Z 值,物体旋转时效果更稳定。追问线性雾和指数雾怎么选?线性雾可控性强,适合有明确近远边界的场景(如走廊)。指数雾只需一个 density 参数,远处自然消融,户外场景首选。指数平方雾过渡最柔和,但远处会突然消失,实际项目很少用。雾颜色一定要和背景色一致吗?是的,否则远处的物体会被雾染成另一个颜色,而不是"融入背景"。雾颜色 = 清屏颜色 = 天空盒颜色,三者必须统一。雾效能用来做性能优化吗?可以。远处的物体被雾覆盖后几乎看不见,可以降低远处物体的 LOD 级别甚至不渲染,雾正好遮住裁剪的接缝——这是开放世界游戏的常用技巧。Three.js 里的 Fog 和 FogExp2 有什么区别?THREE.Fog(color, near, far) 是线性雾,THREE.FogExp2(color, density) 是指数雾。设置 scene.fog = new THREE.Fog(...) 后所有材质自动应用,不需要改着色器。自定义 ShaderMaterial 需要手动读取 fogColor/fogDensity/fogFar/fogNear uniform。如何实现高度雾(Height Fog)?标准雾只看距离,高度雾额外考虑世界空间 Y 坐标:低处雾浓、高处雾淡。片段着色器中用 worldPos.y 做第二次混合,两个因子相乘就是最终雾浓度。
服务端阅读 05月29日 22:14

WebGL Cubemap 立方体贴图是什么?有哪些应用场景?

Cubemap 是 6 张正方形图片拼成的纹理盒子,用 3D 方向向量采样——GPU 根据向量哪个分量绝对值最大决定落在哪个面上,再换算成 2D 坐标取色。核心用途:天空盒、环境反射(reflect)、环境折射(refract)、菲涅尔效果。6 张图必须同尺寸且为 2 的幂次方,采样前务必设 CLAMP_TO_EDGE 防接缝。追问天空盒为什么必须去掉视图矩阵的平移分量?天空盒模拟无限远的环境,如果跟着相机平移,走两步就穿帮了。只保留旋转:mat4 rotOnly = mat4(mat3(viewMatrix))。reflect 和 refract 的区别?reflect(I, N) 计算反射方向——入射光弹回来,用于镜面/金属。refract(I, N, eta) 计算折射方向——光穿过透明介质弯折,用于玻璃/水。真实材质两者同时存在,用菲涅尔公式混合:正面看折射为主,侧面看反射为主。动态环境映射性能开销大怎么办?每帧渲染 6 个面代价太高。常用优化:降低分辨率(64×64 够了,反射本身就模糊)、降低更新频率(每 5-10 帧更新一次)、只给关键物体开动态反射。静态场景用预过滤环境贴图(Prefiltered Env Map),运行时零计算。Cubemap 接缝怎么处理?99% 是忘了设 CLAMP_TO_EDGE。设了还有缝,检查 6 张图边缘像素是否连续——很多在线生成工具会在接缝处偏移 1 像素。+Y 面图片经常上下颠倒,用 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true) 翻转。Cubemap 和 Equirectangular(经纬度贴图)怎么选?Cubemap 6 张图,GPU 采样效率高,PBR 管线原生支持。Equirectangular 一张图,存储方便但两极有拉伸畸变,采样需要三角函数计算,性能差。实际工作流:用 Equirectangular 存储/传输,运行时转换为 Cubemap 使用。