结合拼多多、小红书、牛客网的前端面试真题,我整理了Promise 高频场景题 + 手撕代码,覆盖拼多多核心考点(并发控制、API 实现、重试机制)、小红书业务场景(批量请求、错误容错)、牛客网笔试题(串行执行、特殊 API),全部是面试可直接背的标准答案。
一、拼多多前端核心 Promise 题(一面/二面必考)
场景题 1:Promise 并发控制(最常考)
题目:实现一个promisePool函数,限制最大并发数max,谁先完成就补下一个,结果顺序与输入一致,全部成功才 resolve,任一失败立即 reject。适配「电商商品批量库存查询」「订单批量提交」场景。
解析:核心是维护运行任务数,用索引控制任务分配,避免重复执行。
手撕代码:
/** * 并发控制 Promise 池 * @param {Array<() => Promise>} tasks 异步任务数组(必须是返回 Promise 的函数) * @param {number} max 最大并发数 * @returns {Promise<any[]>} 结果数组,顺序与 tasks 一致 */functionpromisePool(tasks,max){if(!Array.isArray(tasks)||max<=0)returnPromise.reject(newError('参数非法'));constresults=newArray(tasks.length);// 固定长度,保证顺序letrunning=0;// 正在运行的任务数letindex=0;// 下一个要执行的任务索引letisRejected=false;// 标记是否有任务失败returnnewPromise((resolve,reject)=>{// 执行下一个任务construnNext=()=>{// 所有任务已执行完,且无运行中任务 → 全部完成if(index>=tasks.length&&running===0){returnresolve(results);}// 并发数未满,且还有任务 → 执行while(running<max&&index<tasks.length){constcurrentIndex=index++;consttask=tasks[currentIndex];running++;// 执行任务,捕获成功/失败Promise.resolve(task()).then(res=>{if(isRejected)return;// 已失败,不再处理结果results[currentIndex]=res;}).catch(err=>{if(isRejected)return;isRejected=true;reject(err);// 任一失败,立即 reject}).finally(()=>{running--;runNext();// 任务完成,补下一个});}};runNext();// 启动执行});}// 测试用例(电商库存查询场景)constmockTasks=[()=>newPromise(res=>setTimeout(()=>res({skuId:1,stock:10}),1000)),()=>newPromise(res=>setTimeout(()=>res({skuId:2,stock:0}),500)),()=>newPromise(res=>setTimeout(()=>res({skuId:3,stock:20}),800)),()=>newPromise(res=>setTimeout(()=>res({skuId:4,stock:5}),300)),];// 并发数 2,模拟电商批量查询promisePool(mockTasks,2).then(console.log)// 顺序输出:[{skuId:1,stock:10}, {skuId:2,stock:0}, {skuId:3,stock:20}, {skuId:4,stock:5}].catch(console.error);场景题 2:Promise.retry 重试机制(拼多多暑期实习笔试题)
题目:实现一个Promise.retry方法,失败后自动重试times次,每次重试间隔delay毫秒,重试成功则返回结果,全部失败则 reject。适配「支付接口重试」「网络波动请求重试」场景。
手撕代码:
/** * Promise 重试机制 * @param {() => Promise} fn 要执行的异步函数(返回 Promise) * @param {number} times 重试次数(包含第一次执行,默认 3 次) * @param {number} delay 重试间隔(毫秒,默认 1000) * @returns {Promise<any>} 最终结果 */Promise.retry=function(fn,times=3,delay=1000){returnnewPromise((resolve,reject)=>{// 定义重试函数constattempt=(count)=>{fn().then(resolve)// 成功直接 resolve.catch(err=>{// 重试次数用尽 → rejectif(count>=times){returnreject(newError(`重试${times}次全部失败:${err.message}`));}// 延迟重试setTimeout(()=>{attempt(count+1);},delay);});};attempt(1);// 第一次执行});};// 测试用例(支付接口重试)constpayRequest=()=>newPromise((resolve,reject)=>{Math.random()>0.3?resolve('支付成功'):reject(newError('支付接口超时'));});// 重试 3 次,间隔 500msPromise.retry(payRequest,3,500).then(console.log)// 支付成功(概率 70%).catch(console.error);// 重试 3 次失败时触发手撕题 1:Promise.all 实现(全平台必考)
题目:手写实现Promise.all,要求与原生行为一致:传入可迭代对象,全部成功 resolve 结果数组(顺序与输入一致),任一失败立即 reject;空数组返回空数组;兼容非 Promise 值。
手撕代码:
/** * 手写 Promise.all * @param {Iterable} iterable 可迭代对象(数组、Set 等) * @returns {Promise<any[]>} 结果数组 */functionPromiseAll(iterable){// 校验可迭代对象if(!iterable||!iterable[Symbol.iterator]){returnPromise.reject(newTypeError('参数必须是可迭代对象'));}constarr=Array.from(iterable);constlen=arr.length;constresults=newArray(len);letcompletedCount=0;returnnewPromise((resolve,reject)=>{// 空数组直接 resolveif(len===0)returnresolve(results);arr.forEach((item,index)=>{// 包装成 Promise,兼容普通值、thenablePromise.resolve(item).then(res=>{results[index]=res;completedCount++;// 全部完成 → resolveif(completedCount===len){resolve(results);}}).catch(err=>{// 任一失败 → 立即 rejectreject(err);});});});}// 测试用例PromiseAll([1,Promise.resolve(2),newPromise(res=>setTimeout(()=>res(3),100))]).then(console.log);// [1,2,3]PromiseAll([1,Promise.reject('错误'),3]).catch(console.error);// 错误手撕题 2:Promise.race 实现(拼多多常考变形题)
题目:手写实现Promise.race,要求与原生行为一致:返回第一个完成(成功/失败)的 Promise 的结果/原因。
手撕代码:
/** * 手写 Promise.race * @param {Iterable} iterable 可迭代对象 * @returns {Promise<any>} 第一个完成的结果 */functionPromiseRace(iterable){if(!iterable||!iterable[Symbol.iterator]){returnPromise.reject(newTypeError('参数必须是可迭代对象'));}constarr=Array.from(iterable);returnnewPromise((resolve,reject)=>{// 空迭代器 → 永远 pendingif(arr.length===0)return;arr.forEach(item=>{Promise.resolve(item).then(resolve)// 第一个成功的直接 resolve.catch(reject);// 第一个失败的直接 reject});});}// 测试用例constp1=newPromise(res=>setTimeout(()=>res('p1 完成'),100));constp2=newPromise(res=>setTimeout(()=>res('p2 完成'),50));PromiseRace([p1,p2]).then(console.log);// p2 完成二、小红书前端 Promise 场景题(业务场景适配)
小红书侧重批量请求容错、异步流程设计,以下是 2 个高频业务题。
场景题 1:小红书笔记批量发布(Promise.allSettled 应用)
题目:小红书笔记发布时,需同时上传 5 张图片,要求:① 全部上传完成后再提交笔记;② 部分图片上传失败不影响整体流程,需记录失败原因;③ 最终返回所有图片的上传结果(成功/失败)。请实现该逻辑。
手撕代码:
/** * 小红书笔记图片批量上传 * @param {Array<File>} files 图片文件数组 * @returns {Promise<Array<{status: 'fulfilled'/'rejected', url?: string, reason?: Error}>>} 上传结果 */asyncfunctionuploadNoteImages(files){// 模拟上传接口constuploadApi=(file)=>newPromise((resolve,reject)=>{setTimeout(()=>{// 模拟 20% 失败率Math.random()>0.2?resolve(`https://xiaohongshu.com/image/${Date.now()}`):reject(newError(`图片${file.name}上传失败`));},500+Math.random()*500);});// 用 allSettled 处理容错,不因为单张失败终止整体constresults=awaitPromise.allSettled(files.map(file=>uploadApi(file)));// 格式化结果(适配小红书业务端展示)returnresults.map((result,index)=>({status:result.status,url:result.status==='fulfilled'?result.value:undefined,reason:result.status==='rejected'?result.reason:undefined,fileName:files[index].name}));}// 测试用例constmockFiles=[newFile([],'image1.jpg'),newFile([],'image2.jpg'),newFile([],'image3.jpg'),];uploadNoteImages(mockFiles).then(console.log).catch(console.error);场景题 2:小红书用户关注列表分页加载(Promise 串行执行)
题目:小红书关注列表分页加载,需按顺序请求第 1、2、3… 页,每一页请求依赖上一页的cursor,且同一时间只能有一个请求在执行。请实现该串行逻辑。
手撕代码:
/** * 小红书关注列表串行分页请求 * @param {number} pageSize 每页数量 * @param {number} maxPage 最大请求页数 * @returns {Promise<Array<any>>} 所有页的关注列表数据 */functionfetchFollowList(pageSize=10,maxPage=5){letcursor=0;// 分页游标constallData=[];// 存储所有页数据letcurrentPage=1;// 模拟小红书关注列表接口constfollowApi=(cursor,pageSize)=>newPromise((resolve)=>{setTimeout(()=>{// 模拟数据:每页 10 条,第 3 页开始无数据constdata=currentPage<3?Array.from({length:pageSize},(_,i)=>({userId:`user_${cursor+i+1}`,name:`用户${cursor+i+1}`,avatar:`https://avatar.com/${cursor+i+1}`})):[];resolve({data,nextCursor:cursor+pageSize});},300);});// 串行执行分页请求constfetchPage=async()=>{if(currentPage>maxPage)returnallData;const{data,nextCursor}=awaitfollowApi(cursor,pageSize);allData.push(...data);cursor=nextCursor;currentPage++;// 无数据或达到最大页数 → 结束if(data.length===0||currentPage>maxPage){returnallData;}// 串行请求下一页returnfetchPage();};returnfetchPage();}// 测试用例fetchFollowList(10,5).then(console.log)// 输出前 2 页数据(共 20 条).catch(console.error);三、牛客网 Promise 笔试题(笔试高频)
牛客网侧重Promise 细节理解、特殊 API 实现、循环中 Promise 陷阱,以下是 3 道必刷题。
笔试题 1:lastPromise 实现(拼多多笔试真题)
题目:实现lastPromise函数,传入 Iterable 参数,返回最后一个成功的 Promise 的结果,失败的 Promise 跳过;若所有 Promise 都失败,返回字符串all promise is reject。
手撕代码:
/** * 牛客网笔试题:lastPromise * @param {Iterable} iterable 可迭代对象 * @returns {Promise<any | string>} 最后一个成功结果,全失败返回 'all promise is reject' */functionlastPromise(iterable){if(!iterable||!iterable[Symbol.iterator]){returnPromise.reject(newTypeError('参数必须是可迭代对象'));}constarr=Array.from(iterable);if(arr.length===0)returnPromise.resolve('all promise is reject');// 包装所有任务,捕获失败,只保留成功结果constsuccessPromises=arr.map(item=>Promise.resolve(item).catch(()=>null)// 失败返回 null);returnPromise.all(successPromises).then(results=>{// 过滤成功结果constsuccessResults=results.filter(res=>res!==null);if(successResults.length>0){// 返回最后一个成功结果returnsuccessResults[successResults.length-1];}else{// 全失败return'all promise is reject';}});}// 测试用例lastPromise([newPromise(res=>setTimeout(()=>res(9),2000)),1,// 普通值,直接成功newPromise((_,rej)=>rej(2)),// 失败Promise.reject(3)// 失败]).then(console.log);// 1(最后一个成功结果)lastPromise([Promise.reject(1),Promise.reject(2)]).then(console.log);// all promise is reject笔试题 2:Promise 串行执行任务(牛客网高频)
题目:实现serialExecuteTasks函数,传入异步任务数组(每个任务是返回 Promise 的函数),串行执行(前一个完成后执行后一个),全部成功返回结果数组,任一失败立即 reject。
手撕代码:
/** * 牛客网笔试题:Promise 串行执行 * @param {Array<() => Promise>} tasks 异步任务数组 * @returns {Promise<any[]>} 结果数组 */asyncfunctionserialExecuteTasks(tasks){if(!Array.isArray(tasks))returnPromise.reject(newError('参数必须是数组'));constresults=[];for(consttaskoftasks){// 串行执行,等待前一个完成constres=awaittask();results.push(res);}returnresults;}// 测试用例consttasks=[()=>newPromise(res=>setTimeout(()=>res(1),1000)),()=>newPromise(res=>setTimeout(()=>res(2),500)),()=>newPromise(res=>setTimeout(()