JavaScript Promise

Promise 简单介绍

先简单介绍下 Promise
Promise 对象用于表示一个异步操作的最终完成 (或失败), 及其结果值。可以为异步操作的成功和失败绑定执行函数,让异步方法可以像同步方法一样返回值,但立即返回的是一个能代表未来可能出现结果的Promise对象。
Promise 对象有三种状态:

  • pending: 初始状态,既不是成功,也不是失败状态。
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失败。

Promise 的使用和提供的静态方法:

  • new Promise( function(resolve, reject) {...} /* executor */ ); :返回 Promise 对象
  • Promise.all(iterable) :iterable参数对象里所有的promise对象都成功的时候才会触发成功,若一个失败,则立即触发返回Promise对象的失败
  • Promise.race(iterable):iterable参数中的一个成功或者失败都会立即触发返回对象的成功和失败
  • Promise.reject(reason):返回一个状态为失败的Promise对象
  • Promise.resolve(value):返回一个状态由value给定的Promise对象,通常用于将一个值以Promise的方式使用。

下面开始看题

题一

与js事件循环结合出题,如下,写出执行结果

  1. console.log('script start')
  2. async function async1() {
  3. await async2()
  4. console.log('async1 end')
  5. }
  6. async function async2() {console.log('async2 end')}
  7. async1()
  8. setTimeout(function () {console.log('setTimeout')}, 0)
  9. new Promise(resolve => {
  10. console.log('Promise')
  11. resolve()
  12. }).then(function () {
  13. console.log('promise1')
  14. }).then(function () {
  15. console.log('promise2')
  16. })
  17. console.log('script end')
  18. // 结果如下
  19. // script start
  20. // async2 end
  21. // Promise
  22. // script end
  23. // async1 end
  24. // promise1
  25. // promise2
  26. // setTimeout

掌握事件循环机制和明白 Promise.then() 属于微队列,这一类的题目就都是一个套路。

题二

实现如下调用,lazyMan('xxx').sleep(1000).eat('333').sleepFirst(2000) sleepFirst 最先执行。
这题考察如何组合多个 Promise 和链式调用。
可以用数组将 sleep eat 等函数暂存,同时为了能链式调用,所以每个函数需返回 Promise 对象。那么什么时候执行数组中的函数呢?
根据事件循环机制,用 setTimeout 来执行数组中的方法,在定时器的回调函数中相关的事件已经添加到数组中了,链式执行数组中方法前,需要有一个构建一个 Promise 对象来执行 then 方法,可以通过 Promise.resolve() 返回一个 Promise 对象。

  1. function lazyMan(name) {
  2. this.task = [];
  3. this.task.push(() => {
  4. return new Promise(res => {
  5. console.log('name:' + name);res()
  6. })
  7. })
  8. let run = () => {
  9. let sequence = Promise.resolve()
  10. for (const func of this.task) {
  11. sequence = sequence.then(()=>func())
  12. }
  13. }
  14. setTimeout(() => {run()}, 0)
  15. this.eat = (str) => {
  16. this.task.push(() => {
  17. return new (res => {
  18. console.log('eat:' + str);res()
  19. })
  20. })
  21. return this;
  22. }
  23. this.sleep = (time) => {
  24. this.task.push(() => {
  25. return new Promise(res => {
  26. setTimeout(() => {
  27. console.log(`Wake up after ` + time);res()
  28. }, time)
  29. })
  30. })
  31. return this;
  32. }
  33. this.sleepFirst = (time) => {
  34. this.task.unshift(() => {
  35. return new Promise(res => {
  36. setTimeout(() => {
  37. console.log(`sleepFirst up after ` + time);res()
  38. }, time)
  39. })
  40. })
  41. return this;
  42. }
  43. return this;
  44. }

题三

任务队列可不断的添加异步任务(异步任务都是Promise),但只能同时处理5个任务,5个一组执行完成后才能执行下一组,任务队列为空时暂停执行,当有新任务加入则自动执行。

  1. class RunQune{
  2. constructor(){
  3. this.list = []; // 任务队列
  4. this.target = 5; // 并发数量
  5. this.flag = false; // 任务执行状态
  6. this.time = Date.now()
  7. }
  8. async sleep(time){
  9. return new Promise(res=>setTimeout(res,time))
  10. }
  11. // 执行任务
  12. async run(){
  13. while(this.list.length>0){
  14. this.flag = true;
  15. let runList = this.list.splice(0,this.target);
  16. this.time = Date.now()
  17. await this.runItem(runList)
  18. await this.sleep(300) // 模拟执行时间
  19. }
  20. this.flag = false;
  21. }
  22. async runItem(list){
  23. return new Promise((res)=>{
  24. while(list.length>0){
  25. const fn = list.shift();
  26. fn().then().finally(()=>{
  27. if(list.length === 0){
  28. res()
  29. }
  30. })
  31. }
  32. })
  33. }
  34. // 添加任务
  35. push(task){
  36. this.list.push(...task);
  37. !this.flag && this.run()
  38. }
  39. }

这题还可以进一步发散,不需要等待一组完成在执行下一组,只要并发量没有满,就可以加入新的任务执行,实现的思路没太大变化,在 finally 中改为新增任务。

题四

期望id按顺序打印 0 1 2 3 4 ,且只能修改 start 函数。

  1. function start(id) {
  2. execute(id)
  3. }
  4. for (let i = 0; i < 5; i++) {
  5. start(i);
  6. }
  7. function sleep() {
  8. const duration = Math.floor(Math.random() * 500);
  9. return new Promise(resolve => setTimeout(resolve, duration));
  10. }
  11. function execute(id) {
  12. return sleep().then(() => {
  13. console.log("id", id);
  14. });
  15. }

id 的打印是个异步事件,在 setTimeout 回调执行,按照上面的代码,谁的倒计时先结束,id就先打印,那么想要id按顺序打印,就需要将多个异步事件同步执行,promise 的链式调用可以派上用场。代码如下

  1. function start(id) {
  2. // execute(id)
  3. // 第一种:promise 链式调用,execute 函数返回的就是 promise ,所以可以利用这一点,通过 promise.then 依次执行下一个打印
  4. this.promise = this.promise ? this.promise.then(()=>execute(id)) : execute(id)
  5. // 第二种:先用数组存储异步函数,利用事件循环的下一个阶段,即 setTimeout 的回调函数中执行 promise 的链式调用,这方法本质上和第一种是一样的
  6. this.list = this.list ? this.list : []
  7. this.list.push(() => execute(id))
  8. this.t;
  9. if (this.t) clearTimeout(this.t)
  10. this.t = setTimeout(() => {
  11. this.list.reduce((re, fn) => re.then(() => fn()), Promise.resolve())
  12. })
  13. // 第三种:数组存储id的值,在通过 await 异步执行 execute 函数
  14. this.list = this.list ? this.list : []
  15. this.list.push(id)
  16. clearTimeout(this.t)
  17. this.t = setTimeout(async () => {
  18. let _id = this.list.shift()
  19. while (_id !== undefined) {
  20. await execute(_id);
  21. _id = this.list.shift()
  22. }
  23. })
  24. }

题五

手撕源码系列,来手写一个Promise,在动手前需要先了解 Promise/A+ 规范,列举关键部分的规范,详细规范可见文末链接

  1. Promise 的状态:一个 Promise 的当前状态必须为以下三种状态中的一种:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。
  2. 状态迁移:等待态可以迁移至执行态或者拒绝态;执行态和拒绝态不能迁移至其他状态,且必须有一个不可变的终值
  3. then 方法:一个 promise 必须提供一个 then 方法以访问其当前值、终值和据因,then 方法可以被同一个 promise 调用多次。then 方法接收两个参数 onFulfilled, onRejectedonFulfilledonRejected 必须被作为函数调用,且调用不可超过1次。then 方法需返回 Promise 对象

根据这三点实现了一个简化版的 Promise

  1. function MPromise(executor) {
  2. this.status = 'pending'; // pending , fulfilled , rejected
  3. this.data = '' // 当前promise的值,主要用于 then 方法中的 fulfilled , rejected 两种状态的处理
  4. this.resolveFuncList = []; // 使用数组的原因是,一个promise可以同时执行多个 then 方法, 也就会同时存在多个then回调
  5. this.rejectFunc;
  6. const self = this;
  7. function resolve(value) {
  8. // 使用 setTimeout 实现异步
  9. setTimeout(() => {
  10. if (self.status === 'pending') {
  11. self.status = 'fulfilled';
  12. self.data = value;
  13. // 执行 resolve 函数
  14. self.resolveFuncList.forEach(func => {
  15. func(value)
  16. });
  17. }
  18. })
  19. }
  20. function reject(reason) {
  21. setTimeout(() => {
  22. if (self.status === 'pending') {
  23. self.status = 'rejected';
  24. self.data = value;
  25. self.rejectFunc && self.rejectFunc(reason);
  26. }
  27. })
  28. }
  29. try {
  30. executor(resolve, reject)
  31. } catch (error) {
  32. reject(error)
  33. }
  34. }
  35. MPromise.prototype.then = function (onFulfilled, onRejected) {
  36. let promise2;
  37. // 区分不同状态下的处理
  38. if (this.status === 'pending') {
  39. return promise2 = new MPromise((res, rej) => {
  40. this.resolveFuncList.push(function (value) {
  41. let x = onFulfilled(value);
  42. resolvePromise(promise2, x, res, rej)
  43. })
  44. this.rejectFunc = function (reason) {
  45. let x = onRejected(reason);
  46. resolvePromise(promise2, x, res, rej)
  47. }
  48. })
  49. }
  50. if (this.status === 'fulfilled') {
  51. return promise2 = new MPromise((res, rej) => {
  52. setTimeout(() => {
  53. let x = onFulfilled(this.data) // 输出将上一次执行结果
  54. resolvePromise(promise2, x, res, rej)
  55. })
  56. })
  57. }
  58. if (this.status === 'rejected') {
  59. return promise2 = new MPromise((res, rej) => {
  60. setTimeout(() => {
  61. let x = onRejected(this.data)
  62. resolvePromise(promise2, x, res, rej)
  63. })
  64. })
  65. }
  66. }
  67. function resolvePromise(promise2, x, resolve, reject) {
  68. if (x instanceof MPromise) {
  69. if (x.status === 'pending') {
  70. x.then(value => {
  71. resolvePromise(promise2, value, resolve, reject)
  72. }, reason => {
  73. reject(reason)
  74. })
  75. } else {
  76. x.then(resolve, reject)
  77. }
  78. } else {
  79. resolve(x)
  80. }
  81. }

有的因为时间有限,会让手写 Promise 的 api,以下两个就常常被问到

1. 手写一个 Promise.all

  1. /**
  2. * Promise.all Promise进行并行处理
  3. * 参数: promise对象组成的数组作为参数
  4. * 返回值: 返回一个Promise实例
  5. * 当这个数组里的所有promise对象全部进入FulFilled状态的时候,才会resolve。
  6. */
  7. Promise.all = function(promises) {
  8. return new Promise((resolve, reject) => {
  9. let values = []
  10. let count = 0
  11. promises.forEach((promise, index) => {
  12. promise.then(value => {
  13. console.log('value:', value, 'index:', index)
  14. values[index] = value
  15. count++
  16. if (count === promises.length) {
  17. resolve(values)
  18. }
  19. }, reject)
  20. })
  21. })
  22. }

2. 手写一个 Promise.rase

  1. /**
  2. * Promise.race
  3. * 参数: 接收 promise对象组成的数组作为参数
  4. * 返回值: 返回一个Promise实例
  5. * 只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理(取决于哪一个更快)
  6. */
  7. Promise.race = function(promises) {
  8. return new Promise((resolve, reject) => {
  9. promises.forEach((promise) => {
  10. promise.then(resolve, reject);
  11. });
  12. });
  13. }