☰
Promise串联从回调地狱到链式调用:异步编程核心技巧与实战避坑指南
2026/10/15 1:48:30 网站建设 项目流程

1. 从回调地狱到链式调用:Promise串联到底解决了什么问题

刚接触ES6那会儿,我对Promise的理解长期停留在“比回调好看一点”的层面。直到有一次做一个图片预加载模块,需要按顺序加载十几张图,每张图的加载依赖上一张的尺寸信息来决定下一张的裁剪参数,我才真正被Promise的串联能力救了一命。如果你也写过那种层层嵌套、缩进像楼梯一样的回调代码,应该能体会那种“改一行要数半天括号”的痛苦。

Promise串联的核心价值,说白了就一句话:把异步操作的“先后依赖关系”用扁平的链式结构表达出来,而不是用嵌套结构。这听起来像是语法糖,但它带来的可维护性提升是数量级的。举个最直观的对比,假设有三个异步步骤A、B、C,C依赖B的结果,B依赖A的结果:

传统回调写法大概长这样:

stepA(function (resultA) { stepB(resultA, function (resultB) { stepC(resultB, function (resultC) { console.log('最终结果:', resultC); }, onError); }, onError); }, onError);

而Promise串联写法:

stepA() .then(resultA => stepB(resultA)) .then(resultB => stepC(resultB)) .then(resultC => console.log('最终结果:', resultC)) .catch(onError);

两者的逻辑完全一样,但后者的阅读顺序是从上到下一条直线,前者需要你的眼睛在缩进层级之间来回跳跃。当步骤增加到七八个的时候,回调版本的维护成本会急剧上升,而Promise链只是多几行.then而已。

这里有个很多人忽略的关键点:.then的返回值决定了链的走向。这是Promise串联的全部秘密所在,理解了这一点,后面所有的技巧都是自然推论。.then的回调函数返回什么,下一个.then就接收什么。返回一个普通值,下一个.then直接拿到这个值;返回一个Promise,下一个.then会等这个Promise敲定后再执行,并且拿到它敲定后的值。正是这个“返回Promise就自动展开”的机制,让链式调用成为可能。

我见过不少初学者写出这样的代码:

promise.then(result => { anotherAsync(result).then(innerResult => { // 又嵌套了 }); });

这其实又退回了回调地狱的老路。正确的做法是在.then里返回那个Promise,让链条保持扁平:

promise .then(result => anotherAsync(result)) .then(innerResult => { // 这里拿到的才是anotherAsync的结果 });

这个区别看起来微小,但它是区分“真的会用Promise”和“只是把回调换了个写法”的分水岭。我在带新人的时候,通常会让他们先写一个五步的异步流程,用嵌套写法写一遍,再用串联写法写一遍,对比之后基本就再也不会写错了。

Promise串联适合谁来学?我的判断是:只要你在写JavaScript,只要你的代码里有setTimeout、网络请求、文件读写、数据库操作中的任何一种,你就需要掌握它。它不是高级技巧,而是现代JavaScript异步编程的地基。地基打不牢,后面学async/await也是空中楼阁,因为async/await本质上就是Promise链的语法糖。

2..then的返回值规则:串联逻辑的命门所在

2.1 返回普通值、返回Promise、抛出异常的三岔路口

.then的回调函数执行后,会产生三种可能的结果,每种结果对应链的不同走向。我把这个规则整理成一张表,方便你对照记忆:

回调中的行为下一个.then的行为下一个.catch的行为
返回一个普通值(数字、字符串、对象等)立即以该值执行跳过
返回一个Promise等待该Promise敲定,以其结果执行若该Promise被拒绝则执行
抛出异常(throw)跳过以该异常执行
返回一个被拒绝的Promise跳过以该拒绝原因执行

这张表是Promise串联的“交通规则”。我刚开始学的时候,最容易搞混的是“返回Promise”和“返回普通值”的区别。举个例子:

Promise.resolve(1) .then(val => { return val + 1; // 返回普通值2 }) .then(val => { console.log(val); // 输出2 return Promise.resolve(val * 10); // 返回Promise }) .then(val => { console.log(val); // 输出20,Promise被自动展开 });

注意第二个.then里返回的是Promise.resolve(val * 10),但第三个.then拿到的直接是20,而不是一个Promise对象。这个“自动展开”的行为是Promise规范(Promises/A+)明确规定的,它让链式调用可以无缝衔接同步值和异步值。

2.2 为什么返回Promise会被“展开”:微任务队列的调度逻辑

要真正理解这个展开机制,需要稍微深入一点事件循环的层面。JavaScript是单线程的,异步操作靠事件循环调度。Promise的回调不是同步执行的,而是被放入微任务队列(microtask queue)。每次当前执行栈清空后,引擎会清空微任务队列,然后再取下一个宏任务。

当你返回一个Promise时,引擎会调用这个Promise的.then方法(内部操作,不是你自己写的),把后续链的推进逻辑注册为它的回调。也就是说,返回Promise相当于把后续链“挂载”到了那个Promise的敲定事件上。这就是为什么下一个.then会等待,因为它实际上是在等那个Promise的状态变化通知。

这个机制带来的一个实际影响是:链中每个.then之间至少间隔一个微任务。也就是说,即使你返回的是普通值,下一个.then也不会同步执行,而是被调度到微任务队列。这一点在需要精确控制执行顺序的场景下很重要。我踩过一次坑:在一个循环里创建了多条Promise链,期望它们按创建顺序执行,结果因为微任务的调度顺序和创建顺序不完全一致,导致输出顺序偶尔错乱。后来改成用reduce把链串成一条,才稳定下来。

2.3 链中某一步“断了”会怎样:错误穿透与恢复

Promise链有一个很贴心的设计:错误会沿着链向后传递,直到遇到一个能处理它的.catch。这意味着你不需要在每一步都写错误处理,只需要在链的末尾放一个.catch兜底即可。

Promise.resolve('开始') .then(val => { throw new Error('第二步出错了'); }) .then(val => { console.log('这行不会执行'); }) .then(val => { console.log('这行也不会执行'); }) .catch(err => { console.log('捕获到:', err.message); // 输出:捕获到:第二步出错了 });

中间两个.then被直接跳过,控制权交给了.catch。这个行为叫“错误穿透”,它让错误处理变得非常集中和清晰。

但这里有个容易踩的坑:.catch之后链并没有结束。如果你在.catch后面还有.then,它会继续执行,而且接收到的值是.catch回调的返回值。

Promise.reject(new Error('出错了')) .catch(err => { console.log('处理错误:', err.message); return '恢复值'; // 返回一个普通值,链恢复为已敲定状态 }) .then(val => { console.log('继续执行,拿到:', val); // 输出:继续执行,拿到:恢复值 });

这个特性可以用来做“降级处理”:某个异步操作失败了,返回一个默认值让后续流程继续跑。但如果你不希望后续继续执行,就要在.catch里重新throw,或者返回Promise.reject(...)。

我在实际项目里用这个模式做过接口降级:主接口挂了就返回缓存数据,让页面至少能渲染出来,同时在.catch里上报错误日志。代码大概是这样:

fetchMainData() .catch(err => { reportError(err); return getCacheData(); // 降级到缓存 }) .then(data => renderPage(data)) .catch(err => { renderErrorPage(); // 缓存也挂了,显示错误页 });

这种“分段兜底”的写法比在每一步都写错误处理要清爽得多,而且逻辑层次一目了然。

3. 串联中的值传递:从上一个.then到下一个.then的数据流转

3.1 值传递的基本规则与常见误区

Promise链中,值就像接力棒一样从上一个.then传到下一个.then。规则很简单:上一个.then回调的返回值,就是下一个.then回调的参数。但实际写代码时,有几个误区反复出现。

第一个误区是忘记return。这是新手最常犯的错误,也是我自己早期踩过最多的坑:

Promise.resolve(1) .then(val => { val + 1; // 忘记return,实际返回undefined }) .then(val => { console.log(val); // 输出undefined,不是2 });

因为箭头函数如果只有一行表达式,可以省略return,但一旦加了花括号,就必须显式return。我建议在Promise链的.then里永远写显式return,哪怕只有一行,这样能避免很多低级错误。

第二个误区是在.then里修改外部变量而不是返回值:

let result; Promise.resolve(1) .then(val => { result = val + 1; // 修改外部变量,但没有return }) .then(val => { console.log(val); // undefined console.log(result); // 2,但这是副作用,不是链式传递 });

这种写法虽然能“工作”,但它破坏了链式传递的纯粹性,让数据流变得隐式。在复杂链路中,这种隐式传递会让调试变得非常困难,因为你不知道某个值是在哪一步被修改的。

3.2 用对象包装多个值:解决“只能传一个值”的限制

Promise链的一个天然限制是:每个.then只能接收一个参数。但实际业务中,一个步骤往往需要向下传递多个值。比如第一步拿到用户ID和令牌,第二步需要同时用这两个值去请求数据。

解决方案是用对象或数组把多个值“打包”传递:

Promise.resolve({ userId: 123, token: 'abc' }) .then(({ userId, token }) => { return fetchUserData(userId, token).then(userData => { return { userId, token, userData }; // 打包传递 }); }) .then(({ userId, userData }) => { console.log(`用户${userId}的数据:`, userData); });

这里用了对象解构,让代码保持可读性。如果值比较多,我习惯定义一个“上下文对象”,在整个链中逐步填充:

const context = {}; Promise.resolve() .then(() => { return getUserId(); }) .then(userId => { context.userId = userId; return getToken(userId); }) .then(token => { context.token = token; return fetchData(context); }) .then(data => { context.data = data; return processData(context); });

这种“上下文对象”模式在需要传递大量数据的流程中特别好用,而且调试时可以直接打印context看每一步的状态。不过要注意,这种方式本质上是共享可变状态,在并发场景下要小心。如果多条链共享同一个context对象,可能会互相干扰。我的做法是每条链创建独立的context,或者用不可变的方式每一步返回新对象。

3.3 在链中“分流”:如何根据条件走不同的后续步骤

有时候链走到某一步,需要根据条件决定后续走哪条路。Promise本身没有if-else的分支语法,但可以用返回不同Promise的方式实现分流:

checkUserStatus() .then(status => { if (status === 'vip') { return loadVipContent(); } else { return loadNormalContent(); } }) .then(content => { renderContent(content); });

这个模式的关键是:两个分支返回的Promise必须敲定为相同类型的数据,否则后续的.then就要做类型判断,链的简洁性就被破坏了。我在实际项目中会定义一个统一的内容格式,不管VIP还是普通用户,最终都返回{ type, items }这样的结构,后续处理逻辑就不用关心来源了。

还有一种更复杂的分流是“条件跳过某几步”。比如某个配置项开启时才执行额外的校验步骤。这种场景可以用条件返回Promise.resolve()来“空跑”一步:

Promise.resolve(config) .then(cfg => { if (cfg.needValidation) { return validate(cfg); } return cfg; // 跳过校验,直接传递 }) .then(cfg => { return save(cfg); });

这种写法比在链外面写if-else包住整条链要灵活得多,因为分流点可以在链的任意位置。

4. 串联中的错误处理:.catch的位置决定了什么

4.1 链尾统一捕获与中途分段捕获的取舍

.catch放在链的什么位置,直接决定了错误处理的范围。放在链尾,就是“全链兜底”;放在中间,就是“分段处理”。两种方式各有适用场景。

链尾统一捕获适合那种“任何一步出错,整体就算失败”的流程。比如一个表单提交,需要先校验、再上传文件、再写数据库,任何一步挂了整个提交就失败,用户看到统一的错误提示:

validateForm() .then(uploadFile) .then(saveToDatabase) .then(showSuccess) .catch(err => { showError('提交失败,请重试'); logError(err); });

这种写法简洁明了,但缺点是错误信息被“扁平化”了,用户和开发者都只知道“失败了”,不知道具体哪一步失败。如果需要在不同步骤给出不同的用户提示,就要用分段捕获:

validateForm() .catch(err => { showError('表单校验失败:' + err.message); throw err; // 重新抛出,阻止后续执行 }) .then(uploadFile) .catch(err => { showError('文件上传失败,请检查网络'); throw err; }) .then(saveToDatabase) .catch(err => { showError('保存失败,请联系管理员'); throw err; });

注意每个.catch里的throw err,它的作用是让错误继续向后传递,这样后续的.then不会执行。如果不重新抛出,链会“恢复”并继续往下走,这通常不是我们想要的行为。

我个人的经验是:用户可见的流程用分段捕获,内部逻辑用链尾捕获。因为用户需要知道具体哪里出了问题,而内部逻辑只需要知道“失败了”然后统一上报日志即可。

4.2.catch之后链还能继续:恢复与重试的实现

前面提到过,.catch之后链可以继续执行。这个特性可以用来实现自动重试。思路是:在.catch里判断错误类型,如果是可重试的错误,就返回一个新的Promise重新执行;否则继续抛出。

function fetchWithRetry(url, maxRetries = 3) { return fetch(url).catch(err => { if (maxRetries > 0 && isRetryable(err)) { console.log(`重试中,剩余次数:${maxRetries}`); return fetchWithRetry(url, maxRetries - 1); } throw err; // 重试次数用完或不可重试,继续抛出 }); }

这个递归写法利用了“.catch返回Promise会被展开”的机制,让重试逻辑非常紧凑。不过要注意,递归深度不能太大,否则可能栈溢出。虽然Promise的微任务调度会缓解这个问题,但保险起见还是设一个上限。

还有一种“恢复”场景是返回默认值:

getUserConfig() .catch(err => { console.warn('获取配置失败,使用默认配置'); return DEFAULT_CONFIG; // 返回默认值,链恢复 }) .then(config => { applyConfig(config); // 无论成功还是降级,都会执行 });

这种模式在容错性要求高的场景下很实用,比如配置中心挂了不能让整个应用起不来。

4.3 未处理的拒绝:为什么你的控制台会报“UnhandledPromiseRejection”

如果你在浏览器或Node.js里跑Promise代码,肯定见过这个警告:UnhandledPromiseRejectionWarning。它的意思是:一个Promise被拒绝了,但没有任何.catch处理它。

这个警告不是小事。在Node.js的较新版本中,未处理的拒绝会直接导致进程退出。在浏览器里虽然不会崩溃,但说明你的代码有逻辑漏洞。

产生这个警告的常见原因有三种:

第一种是忘记加.catch。链的末尾没有错误处理,任何一步出错都会变成未处理拒绝。解决办法很简单,链尾加.catch。

第二种是**.catch里又抛出了错误,但没有后续的.catch**。比如:

promise .catch(err => { throw new Error('处理失败'); // 这个错误没有被捕获 });

解决办法是在后面再加一个.catch,或者确保.catch里不再抛出。

第三种比较隐蔽:创建了Promise但没有返回它。比如:

function doSomething() { someAsyncOperation(); // 没有return,这个Promise游离在链外 } doSomething() .then(() => { // 这里的链和someAsyncOperation无关 });

someAsyncOperation返回的Promise没有被链住,如果它被拒绝,就会变成未处理拒绝。解决办法是return someAsyncOperation()。

我在排查这类问题时,通常会在.catch里打印完整的错误堆栈,这样能快速定位是哪个Promise出了问题。另外,在Node.js里可以监听process.on('unhandledRejection')事件做全局兜底,但更好的做法还是从源头保证每个Promise都有处理。

5. 串联与并发的选择:什么时候该用Promise.all,什么时候该用链

5.1 串行与并行的性能差异:一个实际的计算

Promise串联是串行执行:第二步等第一步完成,第三步等第二步完成。如果三个步骤各耗时1秒,总耗时就是3秒。但如果这三个步骤之间没有依赖关系,完全可以并行执行,总耗时降到1秒左右。

Promise.all就是干这个的:

// 串行:约3秒 stepA().then(a => stepB(a)).then(b => stepC(b)); // 并行:约1秒 Promise.all([stepA(), stepB(), stepC()]).then(([a, b, c]) => { // 三个结果同时拿到 });

这个性能差异在步骤多、每步耗时长的场景下非常显著。我曾经优化过一个数据聚合接口,原本是串行调用五个下游服务,总耗时2.5秒;改成Promise.all并行后降到0.6秒,效果立竿见影。

但并行不是万能的。如果步骤之间有数据依赖,就必须串行。比如第二步需要第一步返回的ID作为参数,那就没法并行。判断标准很简单:后一步的输入是否依赖前一步的输出。依赖就串行,不依赖就并行。

5.2 混合模式:先并行再串行,或先串行再并行

实际业务中,纯粹的串行或并行都少见,更多是混合模式。比如:先并行获取用户基本信息和权限列表,然后串行地根据权限逐个加载模块。

Promise.all([getUserInfo(), getPermissions()]) .then(([userInfo, permissions]) => { // 先并行拿到两个基础数据 return permissions.reduce((chain, perm) => { return chain.then(() => loadModule(perm)); }, Promise.resolve()); }) .then(() => { console.log('所有模块加载完成'); });

这里用reduce把串行链“折叠”出来,是处理动态数量串行任务的经典模式。因为模块数量不确定,没法写死.then链,只能用reduce动态构建。

反过来,先串行再并行的场景也有。比如先串行地完成登录和令牌获取,然后并行地拉取多个页面的数据:

login() .then(token => { return Promise.all([ fetchPage1(token), fetchPage2(token), fetchPage3(token) ]); }) .then(([page1, page2, page3]) => { renderAll(page1, page2, page3); });

这种“串行获取凭证,并行拉取数据”的模式在需要认证的场景下非常常见。

5.3Promise.all的“快速失败”特性与Promise.allSettled的补充

Promise.all有一个重要特性:只要有一个Promise被拒绝,整个Promise.all就立即被拒绝,其他Promise的结果被丢弃。这叫“快速失败”(fail-fast)。

这个特性在“所有步骤都必须成功”的场景下是合理的,但在“允许部分失败”的场景下就不适用了。比如批量上传十个文件,允许三个失败七个成功,用Promise.all就会因为一个失败而丢掉所有结果。

ES2020引入了Promise.allSettled来解决这个问题:

Promise.allSettled([uploadFile1(), uploadFile2(), uploadFile3()]) .then(results => { results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(`文件${index + 1}上传成功:`, result.value); } else { console.log(`文件${index + 1}上传失败:`, result.reason); } }); });

allSettled会等所有Promise都敲定,然后返回一个数组,每个元素是{ status, value }或{ status, reason }。这样你就能知道每个任务的具体结果,而不是只知道“有失败”。

我在做批量操作时,默认用allSettled而不是all,因为批量操作中部分失败是常态,需要给用户展示详细的成功/失败列表。只有在“一荣俱荣、一损俱损”的场景下才用all。

6. 从Promise链到async/await:串联逻辑的另一种表达

6.1async/await如何改写Promise链

async/await本质上是Promise链的语法糖。任何用.then写的链,都可以改写成async/await的形式。改写规则很简单:每个.then的回调变成一行await,.catch变成try-catch。

// Promise链写法 function process() { return stepA() .then(a => stepB(a)) .then(b => stepC(b)) .then(c => { console.log(c); }) .catch(err => { console.error(err); }); } // async/await写法 async function process() { try { const a = await stepA(); const b = await stepB(a); const c = await stepC(b); console.log(c); } catch (err) { console.error(err); } }

两种写法在功能上完全等价,但async/await的阅读体验更接近同步代码,尤其是当链中有条件分支和循环时,优势更明显。

不过要注意,async/await并没有取代Promise。async函数返回的仍然是Promise,await后面跟的也必须是Promise(或者会被自动包装成Promise的值)。理解Promise串联的机制,才能理解async/await的行为。比如为什么await一个被拒绝的Promise会抛出异常,为什么async函数里的return值会被包装成Promise,这些都需要Promise的知识打底。

6.2 什么时候该用链,什么时候该用async/await

虽然async/await更易读,但Promise链并没有过时。有些场景下链式写法反而更合适。

第一种场景是简单的单步或两步操作。比如:

fetchData().then(data => render(data)).catch(handleError);

这种一行就能写完的链,用async/await反而要写好几行,显得啰嗦。

第二种场景是需要动态构建链。前面提到的用reduce折叠出串行链,这种动态性用async/await很难表达,因为await是静态语法,没法在循环里“动态添加”。

第三种场景是函数式风格的代码。如果你的代码风格偏向函数式,Promise链的.then可以很自然地嵌入管道操作中,而async/await会打断这种风格。

我个人的选择标准是:逻辑简单、步骤少用链;逻辑复杂、有分支循环用async/await。两者不是对立的,在同一个项目里混用完全没问题。比如底层工具函数用Promise链暴露接口,上层业务逻辑用async/await调用。

6.3 一个容易忽略的差异:错误堆栈的可读性

async/await和Promise链在错误堆栈的可读性上有细微差异。async/await的堆栈通常更清晰,因为它保留了同步代码的调用结构;而Promise链的堆栈可能会丢失一些中间帧,尤其是在跨多个.then的情况下。

我在调试复杂链时,如果错误堆栈不够清晰,会临时在.catch里打印每一步的标记:

stepA() .then(a => { console.log('步骤A完成'); return stepB(a); }) .then(b => { console.log('步骤B完成'); return stepC(b); }) .catch(err => { console.error('出错位置:', err.stack); });

这些日志在开发阶段很有用,上线前可以去掉或改成条件输出。另一个技巧是给每个步骤的Promise“打标签”,比如用Promise.resolve().then(() => stepA())包一层,这样堆栈里会多一帧,更容易定位。

7. 实战中踩过的坑与性能优化心得

7.1 在循环中创建Promise链的陷阱

我见过最常见的Promise误用之一,是在for循环里创建链:

for (let i = 0; i < 5; i++) { asyncOperation(i).then(result => { console.log(result); }); }

这段代码的问题不是“不能跑”,而是执行顺序不可控。五个异步操作同时启动,谁先完成谁先打印,输出顺序是随机的。如果业务要求按顺序执行,这种写法就是错的。

正确的串行写法是用reduce:

[0, 1, 2, 3, 4] .reduce((chain, i) => { return chain.then(() => asyncOperation(i)).then(result => { console.log(result); }); }, Promise.resolve());

这样每个操作都会等前一个完成后再启动,输出顺序稳定为0到4。

如果不需要串行,只是要等所有完成,那就用Promise.all:

Promise.all([0, 1, 2, 3, 4].map(i => asyncOperation(i))) .then(results => { console.log(results); // 按数组顺序返回,不是完成顺序 });

注意Promise.all返回的结果数组顺序和输入数组顺序一致,和完成顺序无关。这个特性很实用,不用担心结果错位。

7.2 链过长导致的调试困难与拆分策略

当一个Promise链超过七八步时,调试会变得困难:出错时不知道是哪一步的问题,日志也难打。我的做法是把长链拆成多个短链,每个短链封装成一个函数:

function prepareData() { return stepA().then(stepB).then(stepC); } function processData(data) { return stepD(data).then(stepE).then(stepF); } function saveResult(result) { return stepG(result).then(stepH); } // 主流程 prepareData() .then(processData) .then(saveResult) .catch(handleError);

这样每个函数内部的链很短,容易理解和测试;主流程一目了然,知道有哪几个阶段。出错时也能快速定位是哪个阶段的问题。

另一个技巧是给每个阶段加超时。Promise本身没有超时机制,但可以用Promise.race实现:

function withTimeout(promise, ms) { const timeout = new Promise((_, reject) => { setTimeout(() => reject(new Error('超时')), ms); }); return Promise.race([promise, timeout]); } withTimeout(stepA(), 5000) .then(stepB) .catch(err => { if (err.message === '超时') { // 处理超时 } });

这个模式在调用外部接口时特别有用,避免某个步骤卡死导致整条链挂起。

7.3 避免“Promise反模式”:那些看起来像Promise但不是Promise的写法

最后分享几个我见过的“Promise反模式”,它们看起来用了Promise,但实际上没有发挥Promise的优势,甚至引入了新问题。

反模式一:在.then里嵌套.then。前面提过,这等于退回回调地狱。正确做法是返回Promise让链保持扁平。

反模式二:用Promise包装同步代码。比如Promise.resolve(1 + 1),这没有意义,同步计算直接算就好,不需要Promise。Promise应该只用于真正的异步操作。

反模式三:忘记处理拒绝。每个链都应该有.catch,或者在async函数里有try-catch。未处理的拒绝是定时炸弹。

反模式四:在.then里做太多事。一个.then回调如果超过十几行,就应该拆成独立函数。.then应该只做“接收值、处理、返回值”这一件事。

反模式五:用Promise做事件订阅。Promise是一次性的,敲定后状态不再变化。如果需要多次响应(比如按钮点击、WebSocket消息),应该用事件监听或Observable,而不是Promise。

这些反模式的共同点是:没有理解Promise的“一次性、链式、可组合”这三个核心特性。理解了这三点,自然就能写出地道的Promise代码。

我在实际项目里,每次code review看到Promise相关的代码,都会重点看三件事:链是否扁平、错误是否处理、值传递是否清晰。这三点过关了,代码质量基本就有保障。Promise串联不是什么高深技术,但它是JavaScript异步编程的基本功,值得花时间把它彻底搞明白。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询