Promise详解:异步操作的核心机制

Promise构造函数语法

new Promise(resolveFunc, rejectFunc) {
  // 异步操作代码
}
  • resolveFunc: 一个函数,当异步操作成功时调用,将Promise状态改为fulfilled
  • rejectFunc: 一个函数,当异步操作失败时调用,将Promise状态改为rejected
resolveFunc(value);  // 操作成功,传入成功值
rejectFunc(reason);  // 操作失败,传入失败原因

参数含义

  • value: 任何JavaScript值,表示操作成功的结果
  • reason: 通常是Error对象,表示操作失败的原因

特殊情况

  • 如果value是另一个Promise对象,则当前Promise的状态会与该Promise保持一致
  • 如果省略参数,Promise将以undefined值完成或拒绝
  1. Promise的生命周期

  2. 创建阶段:构造函数实例化Promise对象,同时生成与之绑定的resolveFunc和rejectFunc函数

  3. 执行阶段:executor函数立即同步执行,通常会启动某个异步任务

  4. 状态改变:异步任务完成后,通过调用resolveFunc或rejectFunc改变Promise状态

  5. 后续处理:Promise状态确定后,触发通过then()、catch()或finally()注册的回调函数

Promise状态特性:

  • 单向流转:Promise一旦从pending转为fulfilled或rejected,状态就不可再变
  • 一次解析:只有第一次调用resolveFunc或rejectFunc才会影响Promise状态,后续调用被忽略
  • 状态独立:Promise状态的改变完全由executor内部决定,与executor函数本身的返回值无关

什么叫"解析一次"?

“解析一次”(resolved once)意味着Promise的状态只能被改变一次。这不是指网络请求只发送一次,而是指一旦Promise的状态从pending变为fulfilled或rejected,后续再调用resolve或reject函数都不会产生效果。

例如,如果你在异步操作中多次调用resolve函数:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve("第一次结果"), 1000);
  setTimeout(() => resolve("第二次结果"), 2000); // 这次调用将被忽略
  setTimeout(() => reject("错误信息"), 3000);    // 这次调用也被忽略
});

promise.then(value => console.log(value)); // 只会输出"第一次结果"

应用场景与代码演示

场景一:封装传统回调式API:将Node.js中基于回调的文件操作转换为Promise形式

// 基于回调的文件系统API
const fs = require('fs');

// 封装为Promise版本
function readFilePromise(path, options = 'utf8') {
  return new Promise((resolve, reject) => {
    fs.readFile(path, options, (error, data) => {
      if (error) {
        reject(error);
      } else {
        resolve(data);
      }
    });
  });
}

// 使用Promise
readFilePromise('./config.json')
  .then(data => {
    const config = JSON.parse(data);
    console.log('成功读取配置:', config);
    return config;
  })
  .catch(error => {
    console.error('读取配置失败:', error.message);
  });

// 使用async/await简化代码
async function loadConfig() {
  try {
    const data = await readFilePromise('./config.json');
    const config = JSON.parse(data);
    console.log('成功读取配置:', config);
    return config;
  } catch (error) {
    console.error('读取配置失败:', error.message);
    return null;
  }
}

loadConfig();

场景二:实现可控超时的网络请求:给不能设置超时的API添加超时功能

// 创建可超时的fetch函数
function fetchWithTimeout(url, options = {}, timeout = 5000) {
  return new Promise((resolve, reject) => {
    // 设置超时定时器
    const timeoutId = setTimeout(() => {
      reject(new Error(`请求超时: 超过${timeout}ms`));
    }, timeout);
    
    // 发起实际请求
    fetch(url, options)
      .then(response => {
        clearTimeout(timeoutId); // 清除超时定时器
        resolve(response);
      })
      .catch(error => {
        clearTimeout(timeoutId); // 清除超时定时器
        reject(error);
      });
  });
}

// 使用示例:获取用户数据并展示
async function displayUserProfile(userId) {
  try {
    console.log('正在获取用户数据...');
    const response = await fetchWithTimeout(
      `https://api.example.com/users/${userId}`,
      { headers: { 'Authorization': 'Bearer token123' } },
      3000 // 3秒超时
    );
    
    if (!response.ok) {
      throw new Error(`HTTP错误: ${response.status}`);
    }
    
    const userData = await response.json();
    
    // 使用获取的数据
    console.log('用户资料:', userData);
    document.getElementById('username').textContent = userData.name;
    document.getElementById('email').textContent = userData.email;
    document.getElementById('profile-pic').src = userData.avatar;
    
    return userData;
  } catch (error) {
    console.error('获取用户数据失败:', error.message);
    document.getElementById('user-profile').innerHTML = 
      `<div class="error">加载用户数据时出错: ${error.message}</div>`;
    return null;
  }
}

// 调用函数显示用户资料
displayUserProfile('user123');

常见误区

误区一:认为executor中的return会影响Promise结果

// ❌ 错误理解
const promise = new Promise((resolve, reject) => {
  return "成功"; // 这不会影响Promise的状态
});

// ✅ 正确做法
const promise = new Promise((resolve, reject) => {
  resolve("成功"); // 这才会将Promise状态设为fulfilled
});

误区二:忽略Promise的错误处理

// ❌ 错误做法:未处理拒绝状态
fetchData().then(data => {
  processData(data);
});

// ✅ 正确做法:添加错误处理
fetchData()
  .then(data => {
    processData(data);
  })
  .catch(error => {
    console.error("处理数据失败:", error);
  });

误区三:Promise链中断问题

// ❌ 错误做法:链中断
getData()
  .then(data => {
    // 这里忘记return处理结果
    processData(data);
  })
  .then(result => {
    // result实际上是undefined,因为上一个then没有返回值
    displayResult(result);
  });

// ✅ 正确做法:保持链连接
getData()
  .then(data => {
    // 返回处理结果以传递给下一个then
    return processData(data);
  })
  .then(result => {
    displayResult(result);
  });

Promise有几种写法?何时return?

为什么在async函数中很少看到return语句?因为:

  1. async函数会自动将返回值包装为Promise
  2. 在使用await时,通常直接使用变量存储结果,流程更加线性
  3. 对于错误处理,习惯使用try/catch而不是返回错误状态的Promise
    然而,在async函数中使用return是完全有效的,它相当于resolve这个值:
// 这两种方式效果相同
async function foo() {
  return "成功";
}

function foo() {
  return Promise.resolve("成功");
}

假设我们需要从API获取用户数据,然后获取该用户的订单,最后计算订单总额。下面展示不同实现方式

// 模拟API函数
function getUserData(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (userId <= 0) {
        reject(new Error("无效的用户ID"));
        return;
      }
      const userData = { id: userId, name: "张三", email: "zhangsan@example.com" };
      resolve(userData);
    }, 500);
  });
}

function getUserOrders(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (userId <= 0) {
        reject(new Error("无效的用户ID"));
        return;
      }
      const orders = [
        { id: 101, amount: 100 },
        { id: 102, amount: 200 }
      ];
      resolve(orders);
    }, 500);
  });
}

function calculateDiscount(total) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const discount = total > 200 ? 0.1 : 0;
      resolve(discount);
    }, 300);
  });
}

// 1. 清晰的链式写法(推荐)- 每个步骤都有明确的return
getUserData(123)
  .then(user => {
    console.log("获取到用户:", user.name);
    return getUserOrders(user.id); // 必须return,传递Promise到下一步
  })
  .then(orders => {
    console.log("获取到订单数:", orders.length);
    const total = orders.reduce((sum, order) => sum + order.amount, 0);
    console.log("原始订单总额:", total);
    return total; // 必须return,传递计算结果到下一步
  })
  .then(total => {
    return calculateDiscount(total); // 必须return,传递Promise到下一步
  })
  .then(discount => {
    console.log("折扣率:", discount * 100 + "%");
    // 没有return,链到此结束
  })
  .catch(error => {
    console.error("操作失败:", error.message);
  });

// 2. 简洁的箭头函数写法(推荐用于简单操作)
console.log("\n简洁写法示例:");
getUserData(123)
  .then(user => getUserOrders(user.id)) // 单行箭头函数隐式return
  .then(orders => orders.reduce((sum, order) => sum + order.amount, 0)) // 隐式return
  .then(total => calculateDiscount(total)) // 隐式return
  .then(discount => console.log("折扣率:", discount * 100 + "%")) // 没有return值
  .catch(error => console.error("操作失败:", error.message));

// 3. 使用中间变量存储Promise的写法(不推荐,但展示了过程)
console.log("\n使用中间变量的写法:");
const userPromise = getUserData(123);

userPromise.then(user => {
  console.log("获取到用户:", user.name);
  
  const ordersPromise = getUserOrders(user.id);
  
  ordersPromise.then(orders => {
    console.log("获取到订单数:", orders.length);
    
    const total = orders.reduce((sum, order) => sum + order.amount, 0);
    console.log("订单总额:", total);
    
    const discountPromise = calculateDiscount(total);
    
    discountPromise.then(discount => {
      console.log("折扣率:", discount * 100 + "%");
      const finalAmount = total * (1 - discount);
      console.log("最终金额:", finalAmount);
    });
  });
});

方式3:使用async/await(最现代的方式)

// 复用上面的Promise版本的函数

async function processUserData(userId) {
  try {
    const user = await getUserData(userId);
    console.log("用户:", user);
    
    const orders = await getUserOrders(user.id);
    const total = orders.reduce((sum, order) => sum + order.amount, 0);
    console.log("订单总额:", total);
    
    return total;
  } catch (error) {
    console.error("操作失败:", error);
    return 0;
  }
}

// 调用async函数
processUserData(123).then(total => {
  console.log("处理完成,总额:", total);
});
何时需要return
  1. 当你需要将值传递给Promise链中的下一个then时
   .then(user => {
     // 处理user数据
     return getUserOrders(user.id); // 将新的Promise传递下去
   })
  1. 当你返回一个Promise对象时
    return getUserOrders(user.id); // 返回Promise,下一个then会等待这个Promise解决
    
  2. 当你计算了新值需要在链中传递时
    .then(orders => {
      const total = orders.reduce((sum, order) => sum + order.amount, 0);
      return total; // 传递计算结果到下一个then
    })
    
何时不需要return
  1. 当使用单行箭头函数时,会自动隐式return

    .then(user => getUserOrders(user.id)) // 等同于 .then(user => { return getUserOrders(user.id); })
    
  2. 当你处于链的最后一个then,且不需要传递值时

    .then(result => {
      console.log("最终结果:", result);
      // 不需要return,链到此结束
    })
    
  3. 在catch或finally块中,除非你想恢复链

    .catch(error => {
      console.error("发生错误:", error);
      // 通常不需要return
    })
    

最佳实践总结

  1. 始终在then回调中使用return,除非:在链的最后一步 / 使用单行箭头函数(隐式return)

  2. 避免创建不必要的中间变量来存储Promise:

    // 不推荐
    const userPromise = getUserData(123);
    userPromise.then(...)
    
    // 推荐
    getUserData(123).then(...)
    
  3. 利用箭头函数简化简单操作

    // 冗长写法
    .then(total => {
      return calculateDiscount(total);
    })
    
    // 简洁写法
    .then(total => calculateDiscount(total))
    // 更简洁 (当参数直接传递时)
    .then(calculateDiscount)
    

函数返回Promise与内联的区别

两种方法的核心区别在于封装方式使用灵活性。下面详细解释这两种方式的区别:

内联executor方式

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve("完成"), 1000);
});

函数返回Promise方式

function createDelayedPromise() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve("完成"), 1000);
  });
}
const promise3 = createDelayedPromise();

区别:

  1. 可重用性: 内联方式:Promise只创建一次,不可重用 函数方式:每次调用函数时创建新的Promise,可以反复调用

  2. 参数: 内联方式*:不能轻松地传递参数(除非使用外部作用域的变量)函数方式:可以接受参数来自定义Promise行为

  3. 执行时机: 内联方式:Promise在定义时立即创建并开始执行 函数方式:只有在函数被调用时才创建并执行Promise

  4. 作用域和封装: 内联方式:可以直接访问外部作用域的变量 函数方式:提供更好的封装,内部变量作用域限制在函数内

  5. 代码组织: 内联方式:适合一次性使用的简单异步操作 函数方式:适合需要重复使用的复杂异步操作

function fetchData(url) {
  return new Promise((resolve, reject) => {
    fetch(url)
      .then(response => response.json())
      .then(resolve)
      .catch(reject);
  });
}

// 可以多次调用并传入不同参数
fetchData('https://api.example.com/users');
fetchData('https://api.example.com/products');

实际应用对比

场景:用户登录
内联方式(适合一次性操作):

// 登录当前用户(特定场景,一次性操作)
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;

const loginPromise = new Promise((resolve, reject) => {
  const xhr = new XMLHttpRequest();
  xhr.open('POST', '/api/login');
  xhr.setRequestHeader('Content-Type', 'application/json');
  xhr.onload = () => xhr.status === 200 ? resolve(JSON.parse(xhr.response)) : reject(xhr.statusText);
  xhr.onerror = () => reject('网络错误');
  xhr.send(JSON.stringify({ username, password }));
});

loginPromise
  .then(user => console.log('登录成功:', user))
  .catch(error => console.error('登录失败:', error));

函数方式(适合可重用操作):

// 可重用的登录函数
function login(username, password) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', '/api/login');
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.onload = () => xhr.status === 200 ? resolve(JSON.parse(xhr.response)) : reject(xhr.statusText);
    xhr.onerror = () => reject('网络错误');
    xhr.send(JSON.stringify({ username, password }));
  });
}

// 可以在应用的不同部分重复使用
login('user1', 'pass123')
  .then(user => console.log('用户1登录成功:', user))
  .catch(error => console.error('用户1登录失败:', error));

// 在另一个地方再次使用
document.getElementById('loginBtn').addEventListener('click', () => {
  const username = document.getElementById('username').value;
  const password = document.getElementById('password').value;
  
  login(username, password)
    .then(handleLoginSuccess)
    .catch(handleLoginError);
});

实际应用场景:数据缓存与刷新机制
在前端应用中,我们经常需要获取API数据,并实现缓存机制提高性能:

// 一个带缓存和过期机制的API请求函数
function fetchWithCache(url, options = {}) {
  const cache = fetchWithCache.cache || (fetchWithCache.cache = new Map());
  const cacheKey = url;
  const cacheTTL = options.cacheTTL || 60000; // 默认缓存1分钟
  
  // 检查是否有有效的缓存数据
  const cachedItem = cache.get(cacheKey);
  if (cachedItem && Date.now() - cachedItem.timestamp < cacheTTL) {
    console.log(`[缓存] 使用缓存数据: ${url}`);
    return Promise.resolve(cachedItem.data);
  }
  
  // 没有缓存或缓存过期,发起新请求
  console.log(`[网络] 从API获取: ${url}`);
  return fetch(url, options)
    .then(response => {
      if (!response.ok) {
        throw new Error(`请求失败: ${response.status}`);
      }
      return response.json();
    })
    .then(data => {
      // 保存到缓存
      cache.set(cacheKey, {
        data,
        timestamp: Date.now()
      });
      return data;
    });
}

// 实际使用场景
function loadUserData(userId) {
  return fetchWithCache(`/api/users/${userId}`);
}

function loadProductData(productId) {
  return fetchWithCache(`/api/products/${productId}`);
}

// 在应用中使用
loadUserData(123)
  .then(userData => {
    console.log("用户数据:", userData);
    // 做一些操作...
    
    // 短时间内再次请求同一用户,会使用缓存
    return loadUserData(123);
  })
  .then(cachedUserData => {
    console.log("来自缓存的用户数据:", cachedUserData);
    // 处理缓存数据...
  });

// 同时,在另一部分代码中也可以使用这个函数
document.getElementById('loadBtn').addEventListener('click', () => {
  const productId = document.getElementById('productId').value;
  loadProductData(productId)
    .then(renderProductDetails)
    .catch(handleError);
});

Promise链中的错误处理与catch机制详解

Promise错误传播原理:
在Promise链中,错误会沿着链条向下传播,直到遇到第一个能处理它的rejection处理函数(即.then()的第二个参数或.catch())。这种机制称为"错误冒泡",类似于传统的try-catch块中的错误冒泡。.catch()可以捕获链中任何位置的未处理错误,您可以选择在每个步骤处理特定错误(使用.then()的第二个参数),也可以在链末尾使用.catch()集中处理所有错误。两种方法可以混合使用

错误处理策略与模式

  1. 逐层处理错误

可以在每个.then()中提供第二个参数来处理错误。允许在每一步定制错误处理逻辑。举个例子,这段代码展开

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("foo");
  }, 300);
});

myPromise
  .then(handleFulfilledA, handleRejectedA)
  .then(handleFulfilledB, handleRejectedB)
  .then(handleFulfilledC, handleRejectedC);

如果展开,不使用独立的命名处理函数:

myPromise
  .then(
    // handleFulfilledA的实现
    function(result) {
      console.log("A处理成功状态:", result);
      // 假设这里对结果进行某些处理
      return result * 2; // 返回变换后的结果给下一个then
    },
    // handleRejectedA的实现
    function(error) {
      console.error("A处理失败状态:", error);
      // 可以选择恢复链(返回一个值)
      return 10; // 这会将Promise链恢复到fulfilled状态
      // 或者继续拒绝(抛出错误或返回拒绝的Promise)
      // throw new Error('仍然失败');
    }
  )
  .then(
    // handleFulfilledB的实现
    function(result) {
      console.log("B处理成功状态:", result);
      // 这里可能会产生新的错误
      if (result < 20) {
        throw new Error('结果太小');
      }
      return result + 5;
    },
    // handleRejectedB的实现
    function(error) {
      console.error("B处理失败状态:", error);
      // 同样可以选择恢复或继续拒绝
      return 25; // 链恢复到fulfilled状态
    }
  )
  .then(
    // handleFulfilledC的实现
    function(result) {
      console.log("C处理成功状态:", result);
      return "最终结果: " + result;
    },
    // handleRejectedC的实现
    function(error) {
      console.error("C处理失败状态:", error);
      return "错误处理后的结果";
    }
  );
  1. 集中式错误处理
    可以使用.catch()在链的末尾处理所有错误:
myPromise
  .then(function(result) {
    console.log("A处理成功:", result);
    return result * 2;
  })
  .then(function(result) {
    console.log("B处理成功:", result);
    if (result < 20) {
      throw new Error('结果太小');
    }
    return result + 5;
  })
  .then(function(result) {
    console.log("C处理成功:", result);
    return "最终结果: " + result;
  })
  .catch(function(error) {
    // 处理链中任何地方发生的错误
    console.error("链中有错误发生:", error);
    return "错误已处理";
  });

在rejection处理函数中,可以:

  1. 继续失败状态:通过抛出新错误或返回一个rejected Promise
   .catch(error => {
     throw new Error('处理错误失败');
     // 或 return Promise.reject(new Error('处理错误失败'));
   })
  1. 恢复到成功状态:通过返回一个正常值或resolved Promise
   .catch(error => {
     console.log('处理了错误,恢复正常流程');
     return '恢复值'; // 链将继续以fulfilled状态执行
   })

错误恢复后继续链

fetchData()
  .then(data => {
    // 正常处理数据
    return processData(data);
  })
  .catch(error => {
    console.error("获取或处理数据时出错:", error);
    // 恢复到默认数据
    return getDefaultData();
  })
  .then(result => {
    // 这里的result可能来自processData(data)或getDefaultData()
    displayResult(result);
  });

错误分类处理

fetchUserData(userId)
  .then(userData => {
    return updateUserProfile(userData);
  })
  .catch(error => {
    if (error.name === 'NetworkError') {
      showOfflineMessage();
      return Promise.reject(error); // 继续拒绝状态
    } else if (error.name === 'AuthError') {
      redirectToLogin();
      return Promise.reject(error); // 继续拒绝状态
    } else {
      // 其他错误类型可以尝试恢复
      console.warn("非致命错误,使用缓存数据:", error);
      return getCachedUserData(userId);
    }
  })
  .then(profile => {
    // 只有在没有NetworkError或AuthError的情况下才会执行
    renderUserProfile(profile);
  })
  .catch(error => {
    // 处理所有未恢复的错误
    console.error("无法显示用户配置文件:", error);
    showErrorPage();
  });
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐