🔥🔥一个小时带你完全掌握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的示例

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

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

scss 复制代码
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 链式调用

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

javascript 复制代码
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 对象上的,而不是传入一个函数中。

javascript 复制代码
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会发生什么呢?

javascript 复制代码
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,但不会阻塞链式的调用

javascript 复制代码
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 会立即被拒绝。

typescript 复制代码
// 创建多个 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 的解决或拒绝而立即解决或拒绝。

typescript 复制代码
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 结果的数组。

javascript 复制代码
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 的拒绝原因。

typescript 复制代码
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来对结果进行竞争即可。

typescript 复制代码
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来做。

javascript 复制代码
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: 根据传进的函数调用,决定状态,可以看下面的面试题。
  • 其它的内置函数一样
ini 复制代码
let a = Promise.resolve(1)
a.then(v => {
  console.log(v)
})

这里的执行顺序如下:

  • 开启事件循环,执行宏任务。
  • 第一行,a 等于一个 reslovedpromise
  • 执行 a.then 注册回调函数,由于a已经 reslove,将回调函数放入微任务队列等待执行
  • 宏任务执行完毕,执行当前事件循环中的所有微任务,则执行回调函数
  • 输出1

3.2 面试题

javascript 复制代码
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
javascript 复制代码
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。
javascript 复制代码
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
javascript 复制代码
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

javascript 复制代码
// 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

  1. promise一共有三个状态,先定义一下:

    ini 复制代码
    // 定义状态常量(成功fulfilled 失败rejected 等待pending),初始化为pending。
    const PENDING = 'pending'
    const FULFILLED = 'fulfilled'
    const REJECTED = 'rejected'
  1. 实现 new promise() 逻辑,传入一个执行异步代码的函数,参数为:resolvereject

    kotlin 复制代码
    class MyPromise {
      _statusMap = {
        PENDING: 'pending',
        FULFILLED: 'fulfilled',
        REJECTED: 'rejected'
      }
    ​
      constructor(executor) {
        this.status = this._statusMap.PENDING
        try {
          executor(this.resolve, this.reject)
        } catch (error) {
          this.reject(error)
        }
      }
    ​
      resolve = (value) => {
        
      }
      reject = (error) => {}
    }
    ​

    这里在构造函数中执行传入的excutor,并传入内部的resolvereject函数。初始化状态为 pending

    接下来实现 resolve的逻辑:

    kotlin 复制代码
    class MyPromise {
      _statusMap = {
        PENDING: 'pending',
        FULFILLED: 'fulfilled',
        REJECTED: 'rejected'
      }
    ​
      constructor(executor) {
        this.status = this._statusMap.PENDING
    ​
        // 存储resolve的值
        this.value = null
    ​
        // 成功回调
        this.onFulfilledCallbacks = []
        try {
          executor(this.resolve, this.reject)
        } catch (error) {
          this.reject(error)
        }
      }
    ​
      resolve = (value) => {
        // 状态只能从pending变为fulfilled或rejected,不能逆向变化
        if (this.status !== this._statusMap.PENDING) {
          return
        }
        this.status = this._statusMap.FULFILLED
        this.value = value
        // 执行then方法中传入的回调函数
        this.onFulfilledCallbacks.forEach(fn => fn(this.value))
      }
      reject = (error) => {}
    }
    ​

    resolve 的时候需要将状态从 pending转换为fullfilled,不可逆转,同时存储resolve传入的值,并清空then传入的回调函数(可以链式调用传入多个回调函数)。

    实现reject函数:

    kotlin 复制代码
    class MyPromise {
      _statusMap = {
        PENDING: 'pending',
        FULFILLED: 'fulfilled',
        REJECTED: 'rejected'
      }
    ​
      constructor(executor) {
        this.status = this._statusMap.PENDING
    ​
        // 存储resolve的值
        this.value = null
    ​
        // 成功回调
        this.onFulfilledCallbacks = []
    ​
        // 存储失败的原因
        this.reason = null
    ​
        // 失败回调
        this.onRejectedCallbacks = []
        try {
          executor(this.resolve, this.reject)
        } catch (error) {
          this.reject(error)
        }
      }
    ​
      resolve = (value) => {
        // 状态只能从pending变为fulfilled或rejected,不能逆向变化
        if (this.status !== this._statusMap.PENDING) {
          return
        }
        this.status = this._statusMap.FULFILLED
        this.value = value
        // 执行then方法中传入的回调函数
        this.onFulfilledCallbacks.forEach(fn => fn(this.value))
      }
      reject = (error) => {
        if (this.status !== this._statusMap.PENDING) {
          return
        }
        this.status = this._statusMap.REJECTED
        this.reason = error
        this.onRejectedCallbacks.forEach(fn => fn(this.reason))
      }
    }

    原理同resolve函数。

    接下来实现then函数,注册回调函数:

    kotlin 复制代码
    class MyPromise {
      _statusMap = {
        PENDING: 'pending',
        FULFILLED: 'fulfilled',
        REJECTED: 'rejected'
      }
    ​
      constructor(executor) {
        this.status = this._statusMap.PENDING
    ​
        // 存储resolve的值
        this.value = null
    ​
        // 成功回调
        this.onFulfilledCallbacks = []
    ​
        // 存储失败的原因
        this.reason = null
    ​
        // 失败回调
        this.onRejectedCallbacks = []
    ​
        try {
          executor(this.resolve, this.reject)
        } catch (error) {
          this.reject(error)
        }
      }
    ​
      resolve = value => {
        // 状态只能从pending变为fulfilled或rejected,不能逆向变化
        if (this.status !== this._statusMap.PENDING) {
          return
        }
        this.status = this._statusMap.FULFILLED
        this.value = value
        // 执行then方法中传入的回调函数
        this.onFulfilledCallbacks.forEach(fn => fn(this.value))
      }
      reject = error => {
        if (this.status !== this._statusMap.PENDING) {
          return
        }
        this.status = this._statusMap.REJECTED
        this.reason = error
        this.onRejectedCallbacks.forEach(fn => fn(this.reason))
      }
      then = (onFulfilled, onRejected) => {
        // 判断onFulfilled是否为函数,不是的话则返回resolve的值
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => this.value
    ​
        // 判断onRejected是否为函数,不是的话则抛出错误
        onRejected =
          typeof onRejected === 'function'
            ? onRejected
            : error => {
                throw this.reason
              }
    ​
        //由于Promise的then方法可以被多次调用,所以需要返回一个新的Promise对象
        let promise1 = new MyPromise((resolve, reject) => {
          // 如果当前状态是fulfilled,则执行onFulfilled,将回调函数直接加入微任务队列
          if (this.status === this._statusMap.FULFILLED) {
            setTimeout(() => {
              try {
                let x = onFulfilled(this.value)
                // 看回调函数的返回值是什么,
                // 如果返回的是一个Promise对象,则直接取它的结果作为新的Promise的结果
                // 如果返回的是一个普通值,则直接将这个普通值作为新的Promise的结果
                // 如果是promise1,则循环依赖了
                resolvePromise(promise1, x, resolve, reject)
              } catch (e) {
                reject(e)
              }
            }, 0)
          } else if (this.status === this._statusMap.REJECTED) {
            setTimeout(() => {
              try {
                let x = onRejected(this.reason)
                resolvePromise(promise1, x, resolve, reject)
              } catch (e) {
                reject(e)
              }
            }, 0)
          } else {
            // 如果当前状态是pending,则将onFulfilled和onRejected加入队列,等待状态改变后执行
            this.onFulfilledCallbacks.push(() => {
              try {
                let x = onFulfilled(this.value)
                resolvePromise(promise1, x, resolve, reject)
              } catch (e) {
                reject(e)
              }
            })
    ​
            this.onRejectedCallbacks.push(() => {
              try {
                let x = onRejected(this.reason)
                resolvePromise(promise1, x, resolve, reject)
              } catch (e) {
                reject(e)
              }
            })
          }
        })
        return promise1
      }
    }
    ​
    function resolvePromise(promise1, x, resolve, reject) {
      // 如果promise2和x相等,则抛出TypeError错误
      if (promise1 === x) {
        return reject(new TypeError('Chaining cycle detected for promise'))
      }
      // 如果x是一个Promise对象,则直接取它的结果作为新的Promise的结果
      if (x instanceof MyPromise) {
        x.then(resolve, reject)
      } else {
        resolve(x)
      }
    }
    ​
    // 测试代码
    const promise = new MyPromise((resolve, reject) => {
      console.log(1)
      setTimeout(() => {
        resolve('2')
      }, 1000)
    })
    ​
    promise
      .then(value => {
        console.log(value)
        return value * 2
      })
      .then(value => {
        console.log(value)
      })
    ​

    分几种情况:

    • 没有传入成功回调函数,则将this.value封装成一个函数的返回值,失败回调一样的道理
    • 如果调用then的时候,promise的状态已经是 resolve了,那么就直接将回调函数加入微任务队列了(通过setTimeout),同时处理回调函数的返回值,如果是一个值,则直接使用调用promise1的resolve,将返回值传递给下一个promise;如果是一个promise,直接调用该promise的then方法注册回调函数,回调函数就是promise1resolvereject。也就是说,如果链式调用内部返回的promise状态改变了,则 resolve promise1,进而会指向 promise1的then注册的回调函数,依此类推。
    • reject同上
    • 如果是pending状态,则将回调函数封装一下缓存起来等待resolve。

    从上面的逻辑可以看出来,其实onFulfilledCallbacksonRejectedCallbacks可以不是数组,因为每次调用都返回的是一个新的promise,回调函数也是存储在新的promise上的,各位看管可以修改为普通函数一试。

    catch函数的实现,直接调用then即可:

    kotlin 复制代码
     catch(failCallback) {
        return this.then(undefined, failCallback)
      }

    由于promise支持直接调用resolvereject则增加两个静态方法

    javascript 复制代码
      static resolve(value) {
        return new MyPromise((resolve, reject) => {
          resolve(value)
        })
      }
    ​
      static reject(reason) {
        return new MyPromise((resolve, reject) => {
          reject(reason)
        })
      }

    综上,我们完成了一个较为简单的promise,还没有实现 all,race等静态方法,核心逻辑已经完成。

    各位感兴趣的可以尝试实现这几种静态方法。

    6. 总结

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

相关推荐
霍先生的虚拟宇宙网络22 分钟前
webp 网页如何录屏?
开发语言·前端·javascript
温吞-ing24 分钟前
第十章JavaScript的应用
开发语言·javascript·ecmascript
彪82525 分钟前
第十章 JavaScript的应用 习题
javascript·css·ecmascript·html5
jessezappy42 分钟前
jQuery-Word-Export 使用记录及完整修正文件下载 jquery.wordexport.js
前端·word·jquery·filesaver·word-export
旧林8431 小时前
第八章 利用CSS制作导航菜单
前端·css
yngsqq1 小时前
c#使用高版本8.0步骤
java·前端·c#
小白不太白9502 小时前
设计模式之 模板方法模式
java·设计模式·模板方法模式
Myli_ing2 小时前
考研倒计时-配色+1
前端·javascript·考研
色空大师2 小时前
23种设计模式
java·开发语言·设计模式
余道各努力,千里自同风2 小时前
前端 vue 如何区分开发环境
前端·javascript·vue.js