🔥🔥一个小时带你完全掌握Promise - 超详细解析

1. 背景

JavaScript 的 Promise 是一个非常重要的概念,用于处理异步操作。在解释 Promise 之前,我们需要了解 JavaScript 的异步编程历史以及 Promise 出现的背景。

  1. 异步编程:JavaScript 是一种单线程语言,这意味着它只有一个执行上下文。在早期的 Web 应用程序中,JavaScript 主要用于处理简单的用户交互和页面动态效果。然而,随着 Web 应用程序的复杂性增加,需要处理更多的异步操作,如 AJAX 请求、文件读写、数据库操作等。如果这些操作都是同步进行的,将会导致 JavaScript 线程被阻塞,影响用户体验。

  2. 回调函数:为了解决异步编程的问题,JavaScript 引入了回调函数。回调函数是一种在异步操作完成后执行的函数。通过将回调函数作为参数传递给异步操作,可以在异步操作完成后立即执行相应的逻辑。例如,AJAX 请求就是一个典型的异步操作,通过回调函数可以在请求成功或失败时处理相应的逻辑。

  3. 回调地狱:随着应用程序的复杂性增加,回调函数嵌套层数越来越多,导致代码难以维护和理解。这种现象被称为“回调地狱”。为了解决这个问题,开发者们开始寻找更好的异步编程解决方案。

  4. Promise 的出现:Promise 是一种用于处理异步操作的 JavaScript 对象。它表示一个尚未完成但预期将来会完成的操作。Promise 对象有三个状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。Promise 对象内部维护一个状态和一个结果值。当异步操作成功时,将状态改为 fulfilled,并将结果值保存下来;当异步操作失败时,将状态改为 rejected,并将错误信息保存下来。

  5. Promise 的优点:Promise 提供了一种链式调用的方式来处理异步操作,避免了回调地狱。同时,Promise 还提供了一些静态方法,如 Promise.all() 和 Promise.race(),用于同时处理多个异步操作。这些优点使得 Promise 成为处理 JavaScript 异步编程的首选方案。

  6. ES6 和 ES8:随着 JavaScript 的发展,Promise 在 ES6(ECMAScript 2015)中被正式引入,并在 ES8(ECMAScript 2017)中得到了进一步的完善。现在,Promise 已经成为现代 JavaScript 编程不可或缺的一部分。

JS 的 Promise 出现是为了解决异步编程中回调地狱的问题,提供一种更优雅、更易维护的异步编程解决方案。随着 JavaScript 的发展,Promise 已经成为处理异步操作的标准做法。

2. 基本的使用

本质上 Promise 是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。

加入我们现在有一个异步函数 createGuangJinListAsync ,创建一个广进计划的名单,它接受两个参数,一个是在创建成功时被调用,一个是在出现异常的时候调用。

以下是 使用createGuangJinListAsync的示例

function successCallback(result){
  console.log("报告杰弗瑞,广进计划名单已生成!" + result)
}
function failureCallback(error){
  console.log("我看看是谁在搞事?" + error)
}
createGuangJinListAsync(staffList, successCallback, failureCallback)

如果重写createGuangJinListAsync为promise的形式,可以把回调函数附加在上面:

createGuangJinListAsync(staffList)
  .then(successCallback)
  .catch(failureCallback)

一个promise 对象有以下几种状态,可以通过不同的函数注册对于的回调函数:

  1. pending(进行中) :初始状态,既不是成功,也不是失败状态。

  2. fulfilled(已成功) :意味着操作成功完成。

  3. rejected(已失败) :意味着操作失败。

可以通过以下方式注册对应的回调函数:

  • .then(onFulfilled, onRejected) :当 Promise 成功解决(变为 fulfilled 状态)时,调用 onFulfilled 回调函数;当 Promise 被拒绝(变为 rejected 状态)时,调用 onRejected 回调函数。

  • .catch(onRejected) :这是一个语法糖,用于注册当 Promise 被拒绝时的回调函数,相当于 .then(null, onRejected)

  • .finally(onFinally) :无论 Promise 是解决还是拒绝,都会调用 onFinally 回调函数。这个回调函数不接收任何参数,通常用于执行一些清理工作。

2.1 链式调用

连续执行两个或者多个异步操作是一个常见的需求,在上一个操作执行成功之后,开始下一个的操作,并带着上一步操作所返回的结果。在旧的回调风格中,这种操作会导致经典的回调地域:

const guangJinOneByOne = function(name, callback) {
  setTimeout(function() {
    console.log(`广进计划中,当前执行人:${name},涨薪25%`)
    callback()
  }, 1000)
}
guangJinOneByOne('马杰', function() {
  guangJinOneByOne('庄正直', function() {
    guangJinOneByOne('胡建林', function() {
      console.log('广进计划完成')
    })
  })
})

有了 Promise,我们就可以通过一个 Promise 链来解决这个问题。这就是 Promise API 的优势,因为回调函数是附加到返回的 Promise 对象上的,而不是传入一个函数中。


const guangJinOneByOne = function(name) {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      if (name === '胡建林') {
        reject(name)
      } else {
        resolve(name)
      }
    }, 1000)
  })
}
​
guangJinOneByOne('马杰')
  .then(name => {
    console.log(`广进计划中,当前执行人:${name},涨薪25%`)
    return guangJinOneByOne('庄正直')
  })
  .then(name => {
    console.log(`广进计划中,当前执行人:${name},降薪25%`)
    return guangJinOneByOne('胡建林')
  })
  .then(name => {
    console.log(`广进计划中,当前执行人:${name},完蛋,碰到硬茬了`)
    console.log('广进计划失败')
  })
  .catch(error => {
    console.log('广进计划失败')
    console.log(error + '就是我们的大救星啊!')
  })
// 广进计划中,当前执行人:马杰,涨薪25%
// 广进计划中,当前执行人:庄正直,降薪25%
// 广进计划失败
// 胡建林就是我们的大救星啊!

这样可以通过链式的调用避免了回调地域,使代码更易于理解和维护。

上面的例子中,我们显式的在每一个promise的回调函数中返回了一个新的promise以供下一个回调函数使用,如果不返回值,或者返回的不是promise会发生什么呢?

const guangJinOneByOne = function(name) {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      if (name === '胡建林') {
        reject(name)
      } else {
        resolve(name)
      }
    }, 1000)
  })
}
​
guangJinOneByOne('马杰')
  .then(name => {
    console.log(`广进计划中,当前执行人:${name},涨薪25%`)
  })
  .then(name => {
    console.log(`广进计划中,当前执行人:${name},降薪25%`)
    return guangJinOneByOne('胡建林')
  })
  .then(name => {
    console.log(`广进计划中,当前执行人:${name},完蛋,碰到硬茬了`)
    console.log('广进计划失败')
  })
  .catch(error => {
    console.log('广进计划失败')
    console.log(error + '就是我们的大救星啊!')
  })
  .finally(() => {
    console.log('广为流传')
  })
// 广进计划中,当前执行人:马杰,涨薪25%
// 广进计划中,当前执行人:undefined,降薪25%
// 广进计划失败
// 胡建林就是我们的大救星啊!
// 广为流传

可以看到,如果链式调用中的每一环没有返回任何值,则下一环节接受的值是undefined,但不会阻塞链式的调用

const guangJinOneByOne = function(name) {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      if (name === '胡建林') {
        reject(name)
      } else {
        resolve(name)
      }
    }, 1000)
  })
}
​
guangJinOneByOne('马杰')
  .then(name => {
    console.log(`广进计划中,当前执行人:${name},涨薪25%`)
    return '庄正直'
  })
  .then(name => {
    console.log(`广进计划中,当前执行人:${name},降薪25%`)
    return guangJinOneByOne('胡建林')
  })
  .then(name => {
    console.log(`广进计划中,当前执行人:${name},完蛋,碰到硬茬了`)
    console.log('广进计划失败')
  })
  .catch(error => {
    console.log('广进计划失败')
    console.log(error + '就是我们的大救星啊!')
  })
  .finally(() => {
    console.log('广为流传')
  })
// 广进计划中,当前执行人:马杰,涨薪25%
// 广进计划中,当前执行人:庄正直,降薪25%
// 广进计划失败
// 胡建林就是我们的大救星啊!
// 广为流传

如果返回的不是Promise,而是一个值,则该值会被传递到下一个链式调用的回调函数中。但是第二个链式调用由于是直接返回的字符串,所以不是异步操作,会和第一个链式调用一起输出,间隔一秒后再输出第三个链式调用。

详细的代码执行过程如下:

  1. guangJinOneByOne('马杰')被调用,它返回一个Promise对象,并在1秒后解决。

  2. 第一个.then()回调函数被添加到微任务队列中,等待Promise解决。

  3. 1秒后,Promise解决,马杰被输出,并且返回字符串’庄正直’。

  4. 由于第一个.then()回调没有返回Promise,第二个.then()回调函数立即被添加到微任务队列中,并将在当前微任务队列的执行过程中被执行。

  5. 第二个.then()回调函数接收到’庄正直’,输出相关信息,并返回调用guangJinOneByOne('胡建林')的结果,这是一个新的Promise对象。

  6. 由于guangJinOneByOne('胡建林')返回的Promise是异步解决的,第二个.then()回调函数之后的第三个.then()回调函数不会立即执行,而是要等到新的Promise解决后才会被添加到微任务队列并执行。

  7. 再过1秒后,guangJinOneByOne('胡建林')返回的Promise解决,胡建林被输出,并且表明广进计划失败。

综上,promise的链式调用分为以下几种情况:

  1. 返回一个值:如果.then()的回调函数返回一个非Promise的值,无论是基本类型(如字符串、数字)还是对象,下一个.then()的回调函数会立即被放入当前事件循环的微任务队列中,等待执行。

  2. 没有返回值:如果.then()的回调函数没有返回任何值(即返回undefined),这等同于返回一个已解决的Promise,因此下一个.then()的回调函数也会立即被放入当前事件循环的微任务队列中。

  3. 返回一个Promise:如果.then()的回调函数返回一个Promise,下一个.then()(或.catch.finally)的回调函数将不会立即被放入微任务队列。相反,它将等待返回的Promise解决或拒绝后,才会被放入微任务队列。

  4. Promise被reject:如果Promise在链中的某个地方被拒绝(使用reject),控制将传递给最近的.catch()回调函数。如果没有.catch(),错误将继续传播,直到被捕获或导致程序崩溃。

  5. 使用.catch().catch()方法用于处理链中任何Promise的拒绝。如果在一个.then()之后有一个.catch(),并且在链中的任何地方出现了错误或拒绝,.catch()中的回调函数将会被执行。

  6. 使用.finally().finally()方法用于在Promise链的末尾添加一个回调函数,无论Promise是解决还是拒绝,这个回调函数都会被执行。.finally()不接收任何参数,它通常用于执行清理操作。

2.2 组合

情景1:多个promise都执行完毕再进行后续的工作:Promise.all

如果想要在多个 Promise 都执行完毕后进行后续的工作,可以使用 Promise.all 方法。Promise.all 接受一个包含多个 Promise 实例的数组作为参数,当这个数组中的所有 Promise 都解决(fulfilled)时,Promise.all 返回的 Promise 也会解决。如果数组中有一个 Promise 被拒绝(rejected),Promise.all 返回的 Promise 会立即被拒绝。

// 创建多个 Promise 实例
let promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 1 完成');
  }, 2000);
});
​
let promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 2 完成');
  }, 1000);
});
​
let promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 3 完成');
  }, 3000);
});
​
// 使用 Promise.all 等待所有 Promise 完成后再执行后续工作
Promise.all([promise1, promise2, promise3])
  .then(values => {
    console.log(values); // 输出所有 Promise 解决的值
    // 所有 Promise 都执行完毕后,进行后续的工作
    console.log('所有 Promise 都已完成');
  })
  .catch(error => {
    console.error('至少有一个 Promise 被拒绝:', error);
  });

情景2:只要有一个 Promise 解决就进行后续工作: Promise.race

使用 Promise.race 方法,它可以接受一个包含多个 Promise 实例的数组作为参数。Promise.race 返回的 Promise 会随着数组中任何一个 Promise 的解决或拒绝而立即解决或拒绝。

let promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 1 完成');
  }, 2000);
});
​
let promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 2 完成');
  }, 1000);
});
​
Promise.race([promise1, promise2])
  .then(value => {
    console.log(value); // 输出第一个解决的 Promise 的值
    console.log('至少有一个 Promise 已完成');
  })
  .catch(error => {
    console.error('至少有一个 Promise 被拒绝:', error);
  });

情景3:无论 Promise 是否全部成功,都要获取所有 Promise 的结果: Promise.allSettled

使用 Promise.allSettled 方法,它可以接受一个包含多个 Promise 实例的数组作为参数。Promise.allSettled 会等到所有 Promise 都已解决或拒绝后,返回一个包含每个 Promise 结果的数组。

let promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 1 完成');
  }, 2000);
});
​
let promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('Promise 2 失败');
  }, 1000);
});
​
Promise.allSettled([promise1, promise2])
  .then(results => {
    results.forEach(result => {
      if (result.status === 'fulfilled') {
        console.log(result.value); // 输出解决的 Promise 的值
      } else {
        console.error(result.reason); // 输出拒绝的 Promise 的理由
      }
    });
    console.log('所有 Promise 都已解决或拒绝');
  });

情景4:一个解决或者所有都拒绝: Promise.any

Promise.any 是一个在 ES2021 中引入的 Promise 组合器,它接受一个可迭代的 Promise 实例数组作为输入,并返回一个新的 Promise。这个新的 Promise 会随着数组中任何一个 Promise 的解决而解决,或者在所有 Promise 都被拒绝后拒绝。

当任何一个 Promise 解决时,Promise.any 返回的 Promise 会以那个已解决 Promise 的值来解决。如果所有 Promise 都被拒绝,Promise.any 返回的 Promise 会以一个 AggregateError 实例拒绝,该实例包含了所有 Promise 的拒绝原因。

let promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('Promise 1 失败')
  }, 2000)
})
​
let promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('Promise 2 失败')
  }, 1000)
})
​
let promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('Promise 3 失败')
  }, 3000)
})
​
Promise.any([promise1, promise2, promise3])
  .then(value => {
    console.log(value) // 输出第一个解决的 Promise 的值
    console.log('至少有一个 Promise 已完成')
  })
  .catch(error => {
    console.error('所有 Promise 都被拒绝:', error)
  })
​
// 所有 Promise 都被拒绝: [AggregateError: All promises were rejected] {
//   [errors]: [ 'Promise 1 失败', 'Promise 2 失败', 'Promise 3 失败' ]
// }

2.3 延伸

问题1:如何实现一个带超时的promise

问题的关键是如何理解超时,一定时间之后,如果想要的结果没有满足,那么就要抛出错误,放弃之前的异步结果。

那么就可以使用promise.race来实现,通过一个定时reject的promise来对结果进行竞争即可。

const timeout = (promise, ms) => Promise.race([
  promise,
  new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), ms))
]);
​
timeout(fetch('https://api.example.com'), 5000).then(handleResponse).catch(handleError);

如果是网络请求,且需要在超时的时候,取消此请求,比如axios, 可以结合 cancelToken来做。

import axios from 'axios';
 
// 创建一个取消令牌的源(cancel token source)
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
 
// 发起请求时使用取消令牌
axios.get('your/url', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('请求被取消', thrown.message);
  } else {
    // 处理其他错误
    console.log(thrown);
  }
});
const timeout = (promise, source, delay) => {
  if (!source || !source.cancel) {
    throw new Error('source 必须是一个可取消的Promise')
  }
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error('timeout'))
      // 取消请求
      source.cancel('请求已经取消!')
    }, delay)
  })
  return Promise.race([promise, timeoutPromise])
}

对于axios,其内部是有超时取消请求的机制的,其底层最终会调用xhr.abort(),或者使用fetch中的AbortController来取消网络请求。

问题2:如何实现一个带取消功能的promise

掘金的这位老哥写的挺好的,大家可以参考一下:面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:???

3. 结合事件循环深入理解Promise

3.1 永远返回的是一个promise对象

promise对象的所有函数调用都会返回一个新的promise对象:

  • Promise.resolve : Promise.resolve方法返回一个被解决的Promise对象。它可以将任何值转换为Promise对象

  • Promise.then : 返回的promise的状态取决于回调函数返回的结果,看上文链式调用解析。

  • new Promise: 根据传进的函数调用,决定状态,可以看下面的面试题。

  • 其它的内置函数一样

let a = Promise.resolve(1)
a.then(v => {
  console.log(v)
})

这里的执行顺序如下:

  • 开启事件循环,执行宏任务。

  • 第一行,a 等于一个 reslovedpromise

  • 执行 a.then 注册回调函数,由于a已经 reslove,将回调函数放入微任务队列等待执行

  • 宏任务执行完毕,执行当前事件循环中的所有微任务,则执行回调函数

  • 输出1

3.2 面试题

const promise = new Promise((resolve, reject) => {
  console.log(1);
  resolve('success')
  console.log(2);
});
promise.then(() => {
  console.log(3);
});
console.log(4)

// 依次输出 1, 2, 4, 3
  • 先支持宏任务,也就是全局同步代码,执行new Promise的时候会立即执行传入的函数,此函数为同步代码

  • 输出1

  • promise的状态为 fullfilled

  • 输出2

  • 执行 promise.then, 由于promise已经 resolve , 回调函数放入微任务队列

  • 输出4

  • 宏任务执行完毕,执行微任务

  • 输出3

const promise = new Promise((resolve, reject) => {
  console.log(1)
  console.log(2)
})
promise.then(() => {
  console.log(3)
})
console.log(4)

// 依次输出 1, 2, 4

3 没有输出的原因是,promise一直是pending状态,因此回调函数不会被放入微任务队列,也就不会被执行。这里和链式调用是不同的,链式调用中,回调函数如果没有返回值,则 promise.then 返回的 promise 会立即 resolve。

const promise1 = new Promise((resolve, reject) => {
  console.log('promise1')
  resolve('resolve1')
})
const promise2 = promise1.then(res => {
  console.log(res)
  console.log(promise2)
})
console.log('1', promise1);
console.log('2', promise2);

// promise1
// 1 Promise { 'resolve1' }
// 2 Promise { }
// resolve1
  • 执行宏任务,输出promise1

  • promise1状态变为 fullfilled

  • 支撑promise1.then,由于promise1已经reslove,回调函数放入微任务队列

  • 输出 1, promise的状态为 `fullfilled

  • 由于promise2promise1.then 返回的,且回调函数还没有执行,则状态为pending,输出 2

  • 执行微任务,输出 reslove1

const promise1 = new Promise((resolve, reject) => {
  console.log('promise1')
  resolve('resolve1')
})
const promise2 = promise1.then(res => {
  console.log(res)
})
console.log('1', promise1)
console.log('2', promise2)
setTimeout(() => {
  console.log('3', promise2)
}, 1000)
​
  • 如果在下一个事件循环再输出promise2,就变成了,3 Promise { undefined }。这是因为回调函数执行后,返回的是一个值或者undefined,则promise2直接根据此值状态变为fullfilled,链式调用那里有详解。

通过上面几个题,其实大家只要记住,promise在不同情况下的不同的返回值是什么,状态怎么变化的,就能灵活运用promise了,区区面试题,不过尔尔。

4. 结合 web worker 使用 promise

// worker线程
self.onmessage = function(e) {
  console.log('接收到消息:', e.data)
  let data = e.data
  // 这里执行耗时的计算任务
  const result = setTimeout(() => {
    console.log('计算完成')
    self.postMessage(data * 2)
  }, 2000)
}
​
// 主线程
class WorkerPromise {
  constructor(data) {
    this.promise = new Promise((resolve, reject) => {
      const worker = new Worker('worker.js')
      worker.postMessage(data)
      worker.onmessage = function(e) {
        const result = e.data
        resolve(result)
      }
    })
  }
​
  // Expose then method
  then(onFulfilled, onRejected) {
    return this.promise.then(onFulfilled, onRejected)
  }
}
​
// Usage example
const wf = new WorkerPromise(2)
wf.then(function(result) {
  console.log('Calculation result:', result)
})
​

workerPromise构造函数参数为要计算的数据将woker的处理逻辑隐藏到内部,我们只需要关心计算的结果就行了,不再需要处理 onMessagepostMessage。这个例子并不完善,核心思想有了,可以继续拓展额外的功能,比如错误处理,关闭worker等。

5. 总结

本人深入浅出的讲解了promise的基本用法和核心原理,希望能够帮助大家理解和使用promise,本文可能有一些理解上的错误,请大家批评指正。

作者:盖伦大王
链接:https://juejin.cn/post/7373831659471061030
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。