JavaScript Promise 链式调用与常见陷阱解析

1次阅读

JavaScript Promise 链式调用与常见陷阱解析

本文深入探讨了javascript promise在链式调用中常见的陷阱,特别是当promise的`.then()`方法未被触发时的问题。通过分析错误的promise构造方式(未调用`resolve`或`reject`)以及不当的promise包装,文章提供了使用`.then()`链式调用和`async/await`语法进行正确重构的示例,旨在帮助开发者构建健壮、高效的异步代码。

javaScript异步编程中,Promise是处理异步操作结果的关键工具。然而,不正确的Promise使用方式,尤其是在链式调用和创建Promise实例时,常常会导致.then()回调不被执行,使程序逻辑中断。本文将详细解析这些常见问题,并提供专业的解决方案。

理解Promise的基本工作原理

Promise代表了一个异步操作的最终完成(或失败)及其结果值。一个Promise有三种状态:

  • pending (待定): 初始状态,既没有成功也没有失败。
  • fulfilled (已成功): 异步操作成功完成。
  • rejected (已失败): 异步操作失败。

当一个Promise从pending状态变为fulfilled或rejected时,它会触发相应的.then()或.catch()回调。

陷阱一:未调用resolve或reject的Promise构造函数

许多开发者在使用new Promise()构造函数时,会忽略其核心作用:包装非Promise的异步操作,并通过手动调用resolve或reject来改变Promise的状态。如果构造函数内部的执行器函数(executor function)没有调用resolve或reject,那么这个Promise将永远停留在pending状态,其后续的.then()回调自然也永远不会被执行。

立即学习Java免费学习笔记(深入)”;

错误示例:

new Promise(function () {    // 这里没有调用 resolve 或 reject    updateToDefaultLayerSetting(); }).then(function() {     // 这段代码永远不会执行     console.log("Promise resolved!"); });

在这个例子中,updateToDefaultLayerSetting()函数可能执行了异步操作,但它并没有直接与这个new Promise实例的resolve或reject关联。因此,这个Promise将永远不会改变状态。

正确创建Promise的示例:

当需要将一个基于回调的异步操作转换为Promise时,应确保调用resolve或reject:

new Promise(function (resolve, reject) {   setTimeout(() => {     if (Math.random() > 0.5) {       resolve('操作成功!'); // 成功时调用 resolve     } else {       reject('操作失败'); // 失败时调用 reject     }   }, 1000); }).then(result => {     console.log(result); }).catch(error => {     console.error(error); });

陷阱二:不必要的Promise包装与不当的链式调用

现代javascript中,许多异步API(如fetch、async函数)本身就返回Promise。此时,不应该再使用new Promise()去“包装”这些已经返回Promise的操作,这不仅冗余,还可能导致逻辑错误,如同上述未调用resolve/reject的问题。正确的做法是直接对这些Promise进行链式调用(.then())或使用await。

考虑以下原始代码片段:

JavaScript Promise 链式调用与常见陷阱解析

Sitekick

一个AI登陆页面自动构建器

JavaScript Promise 链式调用与常见陷阱解析 121

查看详情 JavaScript Promise 链式调用与常见陷阱解析

function loadBasemap(layers) {     if (LayerSettings && LayerSettings.hasOwnProperty('basemap') && LayerSettings.basemap.hasOwnProperty('baseMapLayers')) {         new Promise(function () { // 陷阱:未调用 resolve/reject             updateToDefaultLayerSetting();         }).then(function () {             map = new Map({                 basemap: Basemap.fromjsON(LayerSettings.basemap),                 layers: layers             });         });         return new Promise((resolve, reject) => resolve(map)); // 陷阱:过早返回一个可能未定义的map     }     // ... else 分支 }

这里存在两个主要问题:

  1. 第一个new Promise的执行器函数没有调用resolve或reject,导致其.then()永远不会触发。
  2. 函数最后返回的new Promise((resolve, reject) => resolve(map))会立即解析,但此时map对象可能尚未在前面的异步操作中被赋值,或者即使被赋值,也是在不同Promise链中,导致逻辑不同步。

updateToDefaultLayerSetting是一个async函数,它本身就返回一个Promise。因此,我们应该直接利用这个Promise。

重构方案一:使用.then()进行链式调用

通过正确地链式调用Promise,我们可以确保异步操作按预期顺序执行,并将最终结果传递下去。

async function updateToDefaultLayerSetting() {     // ... 保持原样,它是一个 async 函数,会返回一个 Promise     console.log("Calling default");     const result = await actionDefaultBasemap(); // 确保 actionDefaultBasemap 也返回一个 Promise     console.log(result); }  // 重构 loadBasemap function loadBasemap(layers) {   if (LayerSettings && LayerSettings.hasOwnProperty("basemap") && LayerSettings.basemap.hasOwnProperty("baseMapLayers")) {     // 直接调用 updateToDefaultLayerSetting(),它返回一个 Promise     return updateToDefaultLayerSetting().then(function () {       // 当 updateToDefaultLayerSetting 完成后,执行这里的代码       const mapInstance = new Map({         basemap: Basemap.fromjson(LayerSettings.basemap),         layers: layers,       });       return mapInstance; // 返回 mapInstance,作为此 Promise 链的最终结果     });   } else {     // 处理 else 分支,例如返回一个已解析的 Promise 或抛出错误     return Promise.reject(new Error("LayerSettings.basemap.baseMapLayers not found"));   } }

actionDefaultBasemap的重构建议:

actionDefaultBasemap也存在类似问题,它创建了一个不必要的new Promise且未调用resolve/reject。由于portalA.load()返回一个Promise,我们应该直接利用它。

function actionDefaultBasemap() {     let portalA = new Portal(portalConfig);     // 直接对 portalA.load() 返回的 Promise 进行链式调用     return portalA.load().then(function () {         defaultBasemap = portalA.useVectorBasemaps ? portalA.defaultVectorBasemap : portalA.defaultBasemap;         // 返回一个 Promise,确保后续的 .then 能够等待到 defaultBasemap 赋值完成         return new Promise(resolve => { // 这里可以手动创建一个 Promise 来确保后续操作等待             if (LayerSettings.basemap.baseMapLayers[0].hasOwnProperty('url') && typeof defaultBasemap.resourceInfo !== "undefined") {                 LayerSettings.basemap.baseMapLayers[0].id = defaultBasemap.resourceInfo.data.baseMapLayers[0].id;                 LayerSettings.basemap.baseMapLayers[0].title = defaultBasemap.resourceInfo.data.baseMapLayers[0].title;                 LayerSettings.basemap.baseMapLayers[0].url = defaultBasemap.resourceInfo.data.baseMapLayers[0].url;             }             resolve(defaultBasemap); // 确保在所有操作完成后解析         });     }); }

更优的actionDefaultBasemap重构(避免嵌套Promise):

如果LayerSettings的修改是同步的,可以直接在.then回调中完成并返回defaultBasemap。

function actionDefaultBasemap() {     let portalA = new Portal(portalConfig);     return portalA.load().then(() => {         defaultBasemap = portalA.useVectorBasemaps ? portalA.defaultVectorBasemap : portalA.defaultBasemap;         if (LayerSettings.basemap.baseMapLayers[0].hasOwnProperty('url') && typeof defaultBasemap.resourceInfo !== "undefined") {             LayerSettings.basemap.baseMapLayers[0].id = defaultBasemap.resourceInfo.data.baseMapLayers[0].id;             LayerSettings.basemap.baseMapLayers[0].title = defaultBasemap.resourceInfo.data.baseMapLayers[0].title;             LayerSettings.basemap.baseMapLayers[0].url = defaultBasemap.resourceInfo.data.baseMapLayers[0].url;         }         return defaultBasemap; // 直接返回 defaultBasemap     }); }

重构方案二:使用async/await语法

async/await是ES2017引入的语法糖,它建立在Promise之上,使异步代码看起来和行为更像同步代码,大大提高了可读性。

// 重构 loadBasemap async function loadBasemap(layers) {   if (LayerSettings && LayerSettings.hasOwnProperty("basemap") && LayerSettings.basemap.hasOwnProperty("baseMapLayers")) {     await updateToDefaultLayerSetting(); // 等待 updateToDefaultLayerSetting 完成     const mapInstance = new Map({       basemap: Basemap.fromJSON(LayerSettings.basemap),       layers: layers,     });     return mapInstance; // 返回 mapInstance   } else {     // 处理 else 分支     throw new Error("LayerSettings.basemap.baseMapLayers not found");   } }  // 重构 actionDefaultBasemap async function actionDefaultBasemap() {     let portalA = new Portal(portalConfig);     await portalA.load(); // 等待 portalA.load() 完成     defaultBasemap = portalA.useVectorBasemaps ? portalA.defaultVectorBasemap : portalA.defaultBasemap;     if (LayerSettings.basemap.baseMapLayers[0].hasOwnProperty('url') && typeof defaultBasemap.resourceInfo !== "undefined") {         LayerSettings.basemap.baseMapLayers[0].id = defaultBasemap.resourceInfo.data.baseMapLayers[0].id;         LayerSettings.basemap.baseMapLayers[0].title = defaultBasemap.resourceInfo.data.baseMapLayers[0].title;         LayerSettings.basemap.baseMapLayers[0].url = defaultBasemap.resourceInfo.data.baseMapLayers[0].url;     }     return defaultBasemap; // 返回 defaultBasemap }

使用async/await后,doStart函数可以这样调用:

async function doStart(){             try {         var loadedMap = await loadBasemap([layer0]); // 等待 loadBasemap 完成并获取结果         view = loadView(MAP_canvaS_ID, loadedMap); // 直接使用返回的 map 对象         // ... 其他逻辑     } catch (error) {         console.error("加载地图失败:", error);     } }

总结与最佳实践

  1. 避免不必要的new Promise(): 如果一个函数或API已经返回Promise(例如async函数或fetch),请直接使用.then()或await来处理其结果,不要再用new Promise()包裹。
  2. 手动创建Promise时务必调用resolve或reject: 当你确实需要将一个基于回调的异步操作转换为Promise时,确保在执行器函数中适时调用resolve或reject来改变Promise的状态。
  3. 链式调用与返回: 在.then()回调中,如果返回一个值,下一个.then()将接收到这个值;如果返回一个Promise,下一个.then()将等待该Promise解析后才执行。确保你的异步函数总是返回一个Promise(或async函数隐式返回Promise)。
  4. 优先使用async/await: 对于复杂的异步流程,async/await通常能提供更清晰、更易读的代码结构。
  5. 错误处理: 在Promise链的末尾使用.catch(),或在async/await中使用try…catch块,以优雅地处理异步操作中的错误。

通过遵循这些原则,开发者可以有效地避免Promise不进入.then()的问题,构建出更健壮、可维护的JavaScript异步应用。

text=ZqhQzanResources