likes
comments
collection
share

靠做题📝来掌握Promise/async/await

作者站长头像
站长
· 阅读数 16

📕理论知识

关于Promise理论知识有很多优秀的文档和博客,我就不在这里复制粘贴了,这里我推荐小伙伴们可以阅读阮一峰老师ES6教程里的Promise以及async和await。个人觉得阮一峰老师的ES6教程还是相当不错的。

1️⃣宏任务微任务

前期准备

event loop的执行顺序:

  • 一开始整个脚本作为一个宏任务执行
  • 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
  • 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完
  • 执行浏览器UI线程的渲染工作
  • 检查是否有Web Worker任务,有则执行
  • 执行完本轮的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空

微任务包括: MutationObserverPromise.then()或catch()Promise为基础开发的其它技术,比如fetch APIV8的垃圾回收过程、Node独有的process.nextTick

宏任务包括scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

注意⚠️:在所有任务开始的时候,由于宏任务中包括了script,所以浏览器会先执行一个宏任务,在这个过程中你看到的延迟任务(例如setTimeout)将被放到下一轮宏任务中来执行。

第一题

const promise = new Promise((resolve, reject) => {
  console.log(1);
  resolve('success')
  console.log(2);
});
promise.then(() => {
  console.log(3);
});
console.log(4);
  • 从上至下,先遇到new Promise,执行其中的同步代码1
  • 再遇到resolve('success'), 将promise的状态改为了resolved并且将值保存下来
  • 继续执行同步代码2
  • 跳出promise,往下执行,碰到promise.then这个微任务,将其加入微任务队列
  • 执行同步代码4
  • 本轮宏任务全部执行完毕,检查微任务队列,发现promise.then这个微任务且状态为resolved,执行它。 结果:
1 2 4 3

第二题⭐

const fn = () =>
  new Promise((resolve, reject) => {
    console.log(1);
    resolve("success");
  });
console.log("start");
fn().then(res => {
  console.log(res);
});

这里有一个点需要注意⚠之前我们很容易就以为看到new Promise()就执行它的第一个参数函数了,其实这是不对的,我们得注意它是不是被包裹在函数当中(比如上面这道题)如果是的话,只有在函数调用的时候才会执行!

所以只有当fn()出现时,1才会被打印,那么start应该就是先被打印的,调用fn之后才打印1,然后才执行微任务。

结果:

"start"
1
"success"

第三题

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);
  • 从上至下,先遇到new Promise,执行该构造函数中的代码promise1
  • 碰到resolve函数, 将promise1的状态改变为resolved, 并将结果保存下来
  • 碰到promise1.then这个微任务,将它放入微任务队列
  • promise2是一个新的状态为pendingPromise
  • 执行同步代码1, 同时打印出promise1的状态是resolved
  • 执行同步代码2,同时打印出promise2的状态是pending
  • 宏任务执行完毕,查找微任务队列,发现promise1.then这个微任务且状态为resolved,执行它。

结果:

'promise1'
'1' Promise{<resolved>: 'resolve1'}
'2' Promise{<pending>}
'resolve1'

第四题

setTimeout(() => {
  console.log(1)
}, 0)
new Promise((resolve) => {
  console.log(2)
  resolve()
}).then(() => {
  console.log(3)
}).then(() => {
  console.log(4)
})
console.log(5)
  • 一开始碰到定时器,将这个定时器中的函数放到下一个宏任务的延迟队列中等待执行
  • 接下来是new Promise,先执行同步代码2,然后执行resolve将状态改为resolved
  • 接下来碰到then回调3,放入微任务队列,下一个then回调4因为其上一个promise还未执行,状态还是为pending,先不执行。
  • 然后执行同步代码5
  • 然后从微任务队列中取出微任务3并执行,执行完后微任务4才放入微任务队列,然后再执行微任务4
  • 最后执行宏任务1 结果:
2 5 3 4 1

第五题

(1)

setTimeout(() => {
  console.log('timer1');
  setTimeout(() => {
    console.log('timer3')
  }, 0)
}, 0)
setTimeout(() => {
  console.log('timer2')
}, 0)
console.log('start')

(2)

setTimeout(() => {
  console.log('timer1');
  Promise.resolve().then(() => {
    console.log('promise')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
}, 0)
console.log('start')

这两个例子,只是把第一个定时器中的内容换了一下而已,但是输出结果是不一样的。

一个是为定时器timer3,一个是为Promise.then

如果是定时器timer3的话,它会在timer2后执行,而Promise.then却是在timer2之前执行。

因为timer3是宏任务,等3放入宏任务队列时,timer2已经放入宏任务队列了,所以是在timer2后执行。promise.then是微任务,会被放入微任务队列,而在宏任务执行之前必须先把微任务队列中的所有微任务取出来执行,所以promise.then是在宏任务timer2之前执行。

结果为:

'start'
'timer1'
'timer2'
'timer3'
'start'
'timer1'
'promise'
'timer2'

第六题

const first = () => (new Promise((resolve, reject) => {
  console.log(3)      

  let p = new Promise((resolve, reject) => {
    console.log(7)   
    setTimeout(() => {
      console.log(5)  
      resolve(6)    
    }, 0)
    resolve(1)
  })
  
  resolve(2)
  p.then((arg) => {
    console.log(arg)  
  })
}))

first().then((arg) => {
  console.log(arg)   
})
console.log(4)  
  • 首先,可以先找出所有的同步代码,分别是3,7,4所以这三个数会先被打印。
  • 接下来找微任务,先看new Promise,它在最后一行执行了resolve(1),状态已确定,所以下面的p.then也就会被放入微任务队列。
  • 再往下看,first().then因为上面的resolve(2)也会被放入微任务队列,然后依次从微队列中取出执行,依次打印1,2
  • 接下来找宏任务,上面代码中就一个宏任务,就是定时器,所以最后打印5

注意⚠这里有同学可能看到定时器中的resolve(6)会以为状态改变,再次执行p.then。但是记住,promise的状态只能改变一次

结果为:

3 7 4 1 2 5

第七题⭐

setTimeout(() => {
  console.log("0")  
}, 0)
new Promise((resolve,reject)=>{
  console.log("1")  
  resolve()
}).then(()=>{        
  console.log("2")  
  new Promise((resolve,reject)=>{
    console.log("3") 
    resolve()
  }).then(()=>{      
    console.log("4")
  }).then(()=>{
    console.log("5")
  })
}).then(()=>{  
  console.log("6")
})

new Promise((resolve,reject)=>{
  console.log("7")  
  resolve()
}).then(()=>{         
  console.log("8")
})

这道题稍微难一点,这里我以下面这种方式描述可能更容易懂一些:

1.初始:
输出:1 7(同步),把28微任务放入队列
  
同步:[1 7]
宏队列: [0]
微队列: [2 8]

2.取出微队列2,立即执行3(同步),4放入微队列(未执行),5的状态未确定,
  6放入微队列(因为new Promise中执行了resolve(),注意看清楚这里的then对应哪里的回调)
输出:1 7 2 3
  
同步:[1 7]
宏队列: [0]
微队列: [8 4 6]

3.取出微队列88没有其他操作,再取出微队列4, 5状态确定并放入微队列,
输出:1 7 2 3 8 4

同步:[1 7]
宏队列: [0]
微队列: [6 5]

4.取出微队列6, 6没有其他操作,再取出微队列55没有其他操作,最后取出宏队列0
输出:1 7 2 3 8 4 6 5 0

同步:[1 7]
宏队列: [0]
微队列: []

所以最终输出结果为:1 7 2 3 8 4 6 5 0

第八题

OK,接下来我们先看几题代码量少一点的来缓一下😁

Promise.resolve().then(() => {
  console.log('promise1');
  const timer2 = setTimeout(() => {
    console.log('timer2')
  }, 0)
});
const timer1 = setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(() => {
    console.log('promise2')
  })
}, 0)
console.log('start');

这里以流程图的形式描述一下吧

靠做题📝来掌握Promise/async/await

结果为:

'start'
'promise1'
'timer1'
'promise2'
'timer2'

第九题

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 1000)
})
const promise2 = promise1.then(() => {
  throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
setTimeout(() => {
  console.log('promise1', promise1)
  console.log('promise2', promise2)
}, 2000)
  • 从上至下,先执行第一个new Promise中的函数,碰到setTimeout将它加入下一个宏任务列表
  • 跳出new Promise,碰到promise1.then这个微任务,但其状态还是为pending,先不执行
  • promise2是一个新的状态为pendingPromise
  • 执行同步代码console.log('promise1'),且打印出的promise1的状态为pending
  • 执行同步代码console.log('promise2'),且打印出的promise2的状态为pending
  • 碰到第二个定时器,将其放入下一个宏任务列表
  • 第一轮宏任务执行结束,并且没有微任务需要执行,因此执行第二轮宏任务
  • 先执行第一个定时器里的内容,将promise1的状态改为resolved且保存结果并将之前的promise1.then推入微任务队列
  • 该定时器中没有其它的同步代码可执行,因此执行本轮的微任务队列,也就是promise1.then,它抛出了一个错误,且将promise2的状态设置为了rejected
  • 第一个定时器执行完毕,开始执行第二个定时器中的内容
  • 打印出'promise1',且此时promise1的状态为resolved
  • 打印出'promise2',且此时promise2的状态为rejected

结果为:

'promise1' Promise{<pending>}
'promise2' Promise{<pending>}
test5.html:102 Uncaught (in promise) Error: error!!! at test.html:102
'promise1' Promise{<resolved>: "success"}
'promise2' Promise{<rejected>: Error: error!!!}

2️⃣then、catch、finally

前期准备

  • Promise的状态一经改变就不能再改变。
  • .then.catch都会返回一个新的Promise
  • catch不管被连接到哪里,都能捕获上层未捕捉过的错误。
  • Promise中,返回任意一个非 promise 的值都会被包裹成 promise 对象,例如return 2会被包装为return Promise.resolve(2)
  • Promise.then 或者 .catch 可以被调用多次, 但如果Promise内部的状态一经改变,并且有了一个值,那么后续每次调用.then或者.catch的时候都会直接拿到该值。
  • .then 或者 .catchreturn 一个 error 对象并不会抛出错误,所以不会被后续的 .catch 捕获。
  • .then.catch 返回的值不能是 promise 本身,否则会造成死循环。
  • .then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传。
  • .then方法是能接收两个参数的,第一个是处理成功的函数,第二个是处理失败的函数,再某些时候你可以认为catch.then第二个参数的简便写法。
  • .finally方法也是返回一个Promise,他在Promise结束的时候,无论结果为resolved还是rejected,都会执行里面的回调函数。

第一题

const promise = new Promise((resolve, reject) => {
  reject("error");
  resolve("success2");
});
promise.then(res => {
    console.log("then1: ", res);
}).then(res => {
    console.log("then2: ", res);
}).catch(err => {
    console.log("catch: ", err);
}).then(res => {
    console.log("then3: ", res);
})

结果为:

"catch: " "error"
"then3: " undefined

验证了第三个结论,catch不管被连接到哪里,都能捕获上层未捕捉过的错误。

至于then3也会被执行,那是因为catch()也会返回一个Promise,且由于这个Promise没有返回值,所以打印出来的是undefined

第二题

Promise.resolve(1)
  .then(res => {
    console.log(res);
    return 2;
  })
  .catch(err => {
    return 3;
  })
  .then(res => {
    console.log(res);
  });

结果为:

1
2

Promise可以链式调用,不过promise 每次调用 .then 或者 .catch 都会返回一个新的 promise,从而实现了链式调用, 它并不像一般我们任务的链式调用一样return this

上面的输出结果之所以依次打印出12,那是因为resolve(1)之后走的是第一个then方法,并没有走catch里,所以第二个then中的res得到的实际上是第一个then的返回值。

return 2会被包装成resolve(2)

第三题

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('timer')
    resolve('success')
  }, 1000)
})
const start = Date.now();
promise.then(res => {
  console.log(res, Date.now() - start)
})
promise.then(res => {
  console.log(res, Date.now() - start)
})

Promise 的 .then 或者 .catch 可以被调用多次,但这里 Promise 构造函数只执行一次。或者说 promise 内部状态一经改变,并且有了一个值,那么后续每次调用 .then 或者 .catch 都会直接拿到该值。

结果为:

'timer'
'success' 1001
'success' 1002 (这里的差值不一定为定值)

第四题⭐

Promise.resolve().then(() => {
  return new Error('error!!!')
}).then(res => {
  console.log("then: ", res)
}).catch(err => {
  console.log("catch: ", err)
})

这里可能很多同学想到的是进入.catch然后被捕获了错误。但结果并不是这样的,它走的是.then里面:

"then: " "Error: error!!!"

这也验证了第4点和第6点,返回任意一个非 promise 的值都会被包裹成 promise 对象,因此这里的return new Error('error!!!')也被包裹成了return Promise.resolve(new Error('error!!!'))

当然如果你抛出一个错误的话,可以用下面的任意一种:

return Promise.reject(new Error('error!!!'));
// or
throw new Error('error!!!')

第五题⭐

const promise = Promise.resolve().then(() => {
  return promise;
})
promise.catch(console.err)

.then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。

因此结果会报错:

Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>

第六题⭐

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)

原则8.then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传。

第一个then和第二个then中传入的都不是函数,一个是数字类型,一个是对象类型,因此发生了透传,将resolve(1) 的值直接传到最后一个then里。

所以输出结果为:

1

第七题

Promise.reject('err!!!')
  .then((res) => {
    console.log('success', res)
  }, (err) => {
    console.log('error', err)
  }).catch(err => {
    console.log('catch', err)
  })

.then函数中的两个参数:

第一个参数是用来处理Promise成功的函数,第二个则是处理失败的函数。也就是说Promise.resolve('1')的值会进入成功的函数,Promise.reject('2')的值会进入失败的函数。

结果为:

'error' 'error!!!'

它进入的是then()中的第二个参数里面,而如果把第二个参数去掉,就进入了catch()中:

Promise.reject('error!!!')
  .then((res) => {
    console.log('success', res)
  }).catch(err => {
    console.log('catch', err)
  })

结果为:

'catch' 'error!!!'

第八题

接着来看看.finally(),只要记住三个很重要的知识点就可以了:

  1. .finally()方法不管Promise对象最后的状态如何都会执行
  2. .finally()方法的回调函数不接受任何的参数,也就是说你在.finally()函数中是没法知道Promise最终的状态是resolved还是rejected
  3. 它最终返回的默认会是一个上一次的Promise对象值,不过如果抛出的是一个异常则返回异常的Promise对象。
Promise.resolve('1')
  .then(res => {
    console.log(res)
  })
  .finally(() => {
    console.log('finally')
  })
Promise.resolve('2')
  .finally(() => {
    console.log('finally2')
    return '我是finally2返回的值'
  })
  .then(res => {
    console.log('finally2后面的then函数', res)
  })

这两个Promise.finally都会执行,且就算finally2返回了新的值,它后面的then()函数接收到的结果却还是'2',因此打印结果为:

'1'
'finally2'
'finally'
'finally2后面的then函数' '2'

至于为什么finally2的打印要在finally前面,其实在上面例子的第二个Promise中,可以把.finally()看成是.then()。只有当finally执行完之后,下面的.then才能执行,如同第一个Promise中的下面的.finally需要等到上面的.then执行完之后才能执行。

所以第二个Promisefinally就是紧跟在第一个Promise.then后面的微任务,一旦第一个Promise.then微任务执行完,就执行finally微任务。

finally中要是抛出的是一个异常:

Promise.resolve('1')
  .finally(() => {
    console.log('finally1')
    throw new Error('我是finally中抛出的异常')
  })
  .then(res => {
    console.log('finally后面的then函数', res)
  })
  .catch(err => {
    console.log('捕获错误', err)
  })

结果为:

'finally1'
'捕获错误' Error: 我是finally中抛出的异常

但是如果改为return new Error('我是finally中抛出的异常'),打印出来的就是'finally后面的then函数 1'

3️⃣all、race

前期准备

(1)Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。它接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。

const p = Promise.all([p1, p2, p3]);

p的状态由p1p2p3决定,分成两种情况。

  • 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。
  • 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

(2)Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

我们知道如果直接在脚本文件中定义一个Promise,它构造函数的第一个参数是会立即执行的,就像这样:

const p1 = new Promise(r => console.log('立即打印'))

控制台中会立即打印出 “立即打印”。

因此为了控制它什么时候执行,我们可以用一个函数包裹着它,在需要它执行的时候,调用这个函数就可以了:

function runP1 () {
    const p1 = new Promise(r => console.log('立即打印'))
    return p1
}

runP1() // 调用此函数时才执行

OK,接下来就看几道题目吧~

第一题

function runAsync (x) {
    const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
    return p
}
Promise.all([runAsync(1), runAsync(2), runAsync(3)])
  .then(res => console.log(res))

如上面所描述的,只有runAsync(1)runAsync(2)runAsync(3)的状态都变成fulfilledpromise的状态才会变成fulfilled,此时runAsync(1)runAsync(1)runAsync(1)的返回值组成一个数组,传递给promise的回调函数。而且这个结果中数组的顺序和Promise.all()接收到的数组顺序一致。

结果为:

1
2
3
[1, 2, 3]

第二题⭐

function runAsync (x) {
  const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
  return p
}
function runReject (x) {
  const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))
  return p
}
Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
  .then(res => console.log(res))
  .catch(err => console.log(err))

只要Promise.all的数组中有一个被rejectedpromise的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给promise的回调函数。

  • 首先,因为runAsync(1)runAsync(3)的延时为1000,是最短的,所以会先通过定时器打印。
  • 其次,runReject(2)因为延时为2000,最先被reject,先在定时器中打印,然后其rejected状态被catch捕获,再打印错误信息。
  • 最后就是runReject(4)了,因为状态只能改变一次,所以这里它只在定时器中打印了个4.

结果为:

// 1s后输出
1
3
// 2s后输出
2
Error: 2
// 4s后输出
4

第三题

function runAsync (x) {
  const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
  return p
}
Promise.race([runAsync(1), runAsync(2), runAsync(3)])
  .then(res => console.log('result: ', res))
  .catch(err => console.log(err))

其实这道题就是第一题的all改为race。如上所述,只要Promise.race之中有一个实例率先改变状态,Promise的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给Promise的回调函数。

结果为:

1
'result: ' 1
2
3

第四题

function runAsync(x) {
  const p = new Promise(r =>
    setTimeout(() => r(x, console.log(x)), 1000)
  );
  return p;
}
function runReject(x) {
  const p = new Promise((res, rej) =>
    setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)
  );
  return p;
}
Promise.race([runReject(0), runAsync(1), runAsync(2), runAsync(3)])
  .then(res => console.log("result: ", res))
  .catch(err => console.log(err));

runReject(0)最先执行,返回rejected状态以及相应值。 结果为:

0
'Error: 0'
1
2
3

4️⃣async、await

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。具体可阅读async/await

第一题

await一般返回 Promise 对象的处理结果。如果等待的不是 Promise 对象,则返回该值本身。

async function fn () {
  // return await 123
  // 等同于
  return 123
}
fn().then(res => console.log(res))

结果为:

123

第二题⭐

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}
async1();
console.log('start')
  • 首先一进来是创建了两个函数的,我们先不看函数的创建位置,而是看它的调用位置
  • 发现async1函数被调用了,然后去看看调用的内容
  • 执行函数中的同步代码async1 start,之后碰到了await,它会阻塞async1后面代码的执行,因此会先去执行async2中的同步代码async2,然后跳出async1
  • 跳出async1函数后,执行同步代码start
  • 在一轮宏任务全部执行完之后,再来执行刚刚await后面的内容async1 end

在这里可以理解为「紧跟着await后面的语句相当于放到了new Promise中,下一行及之后的语句相当于放在Promise.then中」。

结果为:

'async1 start'
'async2'
'start'
'async1 end'

让我们来看看将await转换为Promise.then的伪代码:

async function async1() {
  console.log("async1 start");
  // 原来代码
  // await async2();
  // console.log("async1 end");
  
  // 转换后代码
  new Promise(resolve => {
    console.log("async2")
    resolve()
  }).then(res => console.log("async1 end"))
}
async function async2() {
  console.log("async2");
}
async1();
console.log("start")

如果把await async2()换成一个new Promise

async function async1() {
  console.log("async1 start");
  new Promise(resolve => {
    console.log('promise')
  })
  console.log("async1 end");
}
async1();
console.log("start")

结果为:

'async start'
'promise'
'async1 end'
'start'

可以看到new Promise()并不会阻塞后面的同步代码async1 end的执行。

第三题

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  setTimeout(() => {
    console.log('timer')
  }, 0)
  console.log("async2");
}
async1();
console.log("start")

定时器始终是最后执行的,它被放到下一条宏任务的延迟队列中。

结果为:

'async1 start'
'async2'
'start'
'async1 end'
'timer'

第四题

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
  setTimeout(() => {
    console.log('timer1')
  }, 0)
}
async function async2() {
  setTimeout(() => {
    console.log('timer2')
  }, 0)
  console.log("async2");
}
async1();
setTimeout(() => {
  console.log('timer3')
}, 0)
console.log("start")
  • 首先执行同步代码console.log("async1 start");
  • 然后await async2();执行async2(),遇到定时器,把timer2放入宏任务队列,打印async2,回到async1()中,await async2();下面的代码放入then回调中(微任务队列),
  • 然后执行async1();下方的代码,把定时器timer3放入宏任务队列,打印start
  • 然后取出微任务队列并执行,打印async1 end,把定时器timer1放入宏任务队列。
  • 最后按照放入宏任务队列的定时器的顺序,再依次打印输出。

结果为:

'async1 start'
'async2'
'start'
'async1 end'
'timer2'
'timer3'
'timer1'

第五题⭐

async function async1 () {
  console.log('async1 start');
  await new Promise(resolve => {
    console.log('promise1')
  })
  console.log('async1 success');
  return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')

async1await后面的Promise是没有返回值的,也就是它的状态始终是pending状态,因此相当于一直在await,却始终没有响应。所以在await之后的内容是不会执行的,也包括async1后面的 .then

结果为:

'script start'
'async1 start'
'promise1'
'script end'

如果给Promise加上resolve的话:

async function async1 () {
  console.log('async1 start');
  await new Promise(resolve => {
    console.log('promise1')
    resolve('promise1 resolve')
  }).then(res => console.log(res))
  console.log('async1 success');
  return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')

现在Promise有了返回值了,因此await后面的内容将会被执行:

'script start'
'async1 start'
'promise1'
'script end'
'promise1 resolve'
'async1 success'
'async1 end'

第六题⭐

async function async1 () {
  console.log('async1 start');
  await new Promise(resolve => {
    console.log('promise1')
    resolve('promise resolve')
  })
  console.log('async1 success');
  return 'async1 end'
}
console.log('srcipt start')
async1().then(res => {
  console.log('res: ', res)
})
new Promise(resolve => {
  console.log('promise2')
  setTimeout(() => {
    console.log('timer')
  })
})

有一点需要注意的,在async1中的new Promise它的resovle的值和async1().then()里的值是没有关系的,很多小伙伴可能看到resovle('promise resolve')就会误以为是async1().then()中的返回值,但其实async1().then()的值是return 'async1 end'的返回值,它是个pending状态的promise

结果为:

srcipt start
async1 start
promise1
promise2
async1 success
res:  async1 end
Promise {<pending>}
timer

第七题

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}

async function async2() {
  console.log("async2");
}

console.log("script start");

setTimeout(function() {
  console.log("setTimeout");
}, 0);

async1();

new Promise(function(resolve) {
  console.log("promise1");
  resolve();
}).then(function() {
  console.log("promise2");
});
console.log('script end')

这道题跟前面的差不多,前面的题能做出来,这题也没什么问题了,直接给出答案:

'script start'
'async1 start'
'async2'
'promise1'
'script end'
'async1 end'
'promise2'
'setTimeout'

第八题

async function testSometing() {
  console.log("执行testSometing");
  return "testSometing";
}

async function testAsync() {
  console.log("执行testAsync");
  return Promise.resolve("hello async");
}

async function test() {
  console.log("test start...");
  const v1 = await testSometing();
  console.log(v1);
  const v2 = await testAsync();
  console.log(v2);
  console.log(v1, v2);
}

test();

var promise = new Promise(resolve => {
  console.log("promise start...");
  resolve("promise");
});
promise.then(val => console.log(val));

console.log("test end...");

结果为:

'test start...'
'执行testSometing'
'promise start...'
'test end...'
'testSometing'
'执行testAsync'
'promise'
'hello async'
'testSometing' 'hello async'

第九题⭐

OK,asyncawait的题目练的差不多啦!最后再来看一下async处理错误的情况:

async function async1 () {
  await async2();
  console.log('async1');
  return 'async1 success'
}
async function async2 () {
  return new Promise((resolve, reject) => {
    console.log('async2')
    reject('error')
  })
}
async1().then(res => console.log(res))

如果在async函数中抛出了错误,则终止错误结果,不会继续向下执行。

结果为:

'async2'
Uncaught (in promise) error

如果改为throw new Error也是一样的:

async function async1 () {
  console.log('async1');
  throw new Error('error!!!')
  return 'async1 success'
}
async1().then(res => console.log(res))

结果为:

'async1'
Uncaught (in promise) Error: error!!!

如果想要使得错误的地方不影响async函数后续的执行的话,可以使用try catch

async function async1 () {
  try {
    await Promise.reject('error!!!')
  } catch(e) {
    console.log(e)
  }
  console.log('async1');
  return Promise.resolve('async1 success')
}
async1().then(res => console.log(res))
console.log('script start')

结果为:

'script start'
'error!!!'
'async1'
'async1 success'

或者可以直接在Promise.reject后面跟着一个catch()方法,输出结果是一样的:

async function async1 () {
  // try {
  //   await Promise.reject('error!!!')
  // } catch(e) {
  //   console.log(e)
  // }
  await Promise.reject('error!!!')
    .catch(e => console.log(e))
  console.log('async1');
  return Promise.resolve('async1 success')
}
async1().then(res => console.log(res))
console.log('script start')

5️⃣综合题

第一题

const first = () => (new Promise((resolve, reject) => {
    console.log(3);
    let p = new Promise((resolve, reject) => {
        console.log(7);
        setTimeout(() => {
            console.log(5);
            resolve(6);
            console.log(p)
        }, 0)
        resolve(1);
    });
    resolve(2);
    p.then((arg) => {
        console.log(arg);
    });
}));
first().then((arg) => {
    console.log(arg);
});
console.log(4);
  • 第一段代码定义的是一个函数,所以我们得看看它是在哪执行的,发现它在4之前,所以可以来看看first函数里面的内容了。
  • 函数first返回的是一个new Promise(),因此先执行里面的同步代码3
  • 接着又遇到了一个new Promise(),直接执行里面的同步代码7
  • 执行完7之后,在p中,遇到了一个定时器,先将它放到下一个宏任务队列里不管它,接着向下走
  • 碰到了resolve(1),这里就把p的状态改为了resolved,且返回值为1,不过这里也先不执行
  • 跳出p,碰到了resolve(2),这里的resolve(2),表示的是把first函数返回的那个Promise的状态改了,也先不管它。
  • 然后碰到了p.then,将它加入本次循环的微任务列表,等待执行
  • 跳出first函数,遇到了first().then(),将它加入本次循环的微任务列表(p.then的后面执行)
  • 然后执行同步代码4
  • 本轮的同步代码全部执行完毕,查找微任务列表,发现p.thenfirst().then(),依次执行,打印出1和2
  • 本轮任务执行完毕了,发现还有一个定时器没有跑完,接着执行这个定时器里的内容,执行同步代码5
  • 然后又遇到了一个resolve(6),它是放在p里的,但是p的状态在之前已经发生过改变了,因此这里就不会再改变,也就是说resolve(6)相当于没任何用处,因此打印出来的pPromise{<resolved>: 1}

结果为:

3
7
4
1
2
5
Promise{<resolved>: 1}

第二题

const async1 = async () => {
  console.log('async1');
  setTimeout(() => {
    console.log('timer1')
  }, 2000)
  await new Promise(resolve => {
    console.log('promise1')
  })
  console.log('async1 end')
  return 'async1 success'
} 
console.log('script start');
async1().then(res => console.log(res));
console.log('script end');
Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .catch(4)
  .then(res => console.log(res))
setTimeout(() => {
  console.log('timer2')
}, 1000)
  • async函数中awaitnew Promise要是没有返回值的话则不执行后面的内容
  • .then函数中的参数期待的是函数,如果不是函数的话会发生透传
  • 注意定时器的延迟时间

结果为:

'script start'
'async1'
'promise1'
'script end'
1
'timer2'
'timer1'

第三题

const p1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve('resolve3');
    console.log('timer1')
  }, 0)
  resolve('resovle1');
  resolve('resolve2');
}).then(res => {
  console.log(res)
  setTimeout(() => {
    console.log(p1)
  }, 1000)
}).finally(res => {
  console.log('finally', res)
})
  • Promise的状态一旦改变就无法改变
  • finally不管Promise的状态是resolved还是rejected都会执行,且它的回调函数是接收不到Promise的结果的,所以finally()中的res是一个迷惑项
  • 最后一个定时器打印出的p1其实是.finally的返回值,我们知道.finally的返回值如果在没有抛出错误的情况下默认会是上一个Promise的返回值, 而这道题中.finally上一个Promise.then(),但是这个.then()并没有返回值,所以p1打印出来的Promise的值会是undefined,如果你在定时器的下面加上一个return 1,则值就会变成1

结果为:

'resolve1'
'finally' undefined
'timer1'
Promise{<resolved>: undefined}

🔥手撕Promise

// 模拟实现Promise
// Promise利用三大手段解决回调地狱:
// 1. 回调函数延迟绑定
// 2. 返回值穿透
// 3. 错误冒泡

// 定义三种状态
const PENDING = 'pending';      // 进行中
const FULFILLED = 'fulfilled';  // 已成功
const REJECTED = 'rejected';    // 已失败

class Promise {
  constructor(exector) {
    this.status = PENDING;    // 初始化状态
    this.value = undefined;   // 将成功、失败结果放在this上,便于then、catch访问
    this.reason = undefined;
    this.onFulfilledCallbacks = [];  // 成功态回调函数队列
    this.onRejectedCallbacks = [];   // 失败态回调函数队列

    const resolve = value => {
      // 只有进行中状态才能更改状态
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        // 成功态函数依次执行
        this.onFulfilledCallbacks.forEach(fn => fn(this.value));
      }
    }
    const reject = reason => {
      // 只有进行中状态才能更改状态
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        // 失败态函数依次执行
        this.onRejectedCallbacks.forEach(fn => fn(this.reason))
      }
    }
    try {
      // 立即执行executor
      // 把内部的resolve和reject传入executor,用户可调用resolve和reject
      exector(resolve, reject);
    } catch(e) {
      // executor执行出错,将错误内容reject抛出去
      reject(e);
    }
  }
  
  //then方法返回的是一个新的Promise实例,第一个参数是`resolved`状态的回调函数,
  //第二个参数是`rejected`状态的回调函数,它们都是可选的。
  then(onFulfilled, onRejected) {
    // 解决 onFufilled,onRejected 没有传值的问题
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function'? onRejected :
      reason => { throw new Error(reason instanceof Error ? reason.message : reason) }
    // 保存this
    const self = this;
    // then回调会返回一个promise
    return new Promise((resolve, reject) => {
      if (self.status === PENDING) {
        self.onFulfilledCallbacks.push(() => {
          // try捕获错误
          try {
            // 模拟微任务
            setTimeout(() => {
              const result = onFulfilled(self.value);
              // 分两种情况:
              // 1. 回调函数返回值是Promise,执行then操作(递归)
              // 2. 如果不是Promise,直接调用Promise的resolve函数
              result instanceof Promise ? result.then(resolve, reject) : resolve(result);
            })
          } catch(e) {
            reject(e);
          }
        });
        self.onRejectedCallbacks.push(() => {
          // 以下同理
          try {
            setTimeout(() => {
              const result = onRejected(self.reason);
              // 不同点:此时是reject
              result instanceof Promise ? result.then(resolve, reject) : resolve(result);
            })
          } catch(e) {
            reject(e);
          }
        })
      } else if (self.status === FULFILLED) {
        try {
          setTimeout(() => {
            const result = onFulfilled(self.value);
            result instanceof Promise ? result.then(resolve, reject) : resolve(result);
          });
        } catch(e) {
          reject(e);
        }
      } else if (self.status === REJECTED) {
        try {
          setTimeout(() => {
            const result = onRejected(self.reason);
            result instanceof Promise ? result.then(resolve, reject) : resolve(result);
          })
        } catch(e) {
          reject(e);
        }
      }
    });
  }
  
  catch(onRejected) {
    return this.then(null, onRejected);
  }
  
  //finally()返回一个Promise,在promise结束时,无论结果是fulfilled还是rejected都会执行指定的回调函数
  //在finally之后,还可以继续then,并且会将值原封不动的传递给后面的then。finally本质上是then方法的特例
  finally (callback) {
      let Promise = this.constructor;
      return this.then(
        value  => Promise.resolve(callback()).then(() => value),
        reason => Promise.resolve(callback()).then(() => { throw reason })
      );
  }
  
  // static静态方法,直接通过类可以调用,不用实例调用
  static resolve(value) {
    if (value instanceof Promise) {
      // 如果是Promise实例,直接返回
      return value;
    } else {
      // 如果不是Promise实例,返回一个新的Promise对象,状态为FULFILLED
      return new Promise((resolve, reject) => resolve(value));
    }
  }
  
  static reject(reason) {
    return new Promise((resolve, reject) => {
      reject(reason);
    })
  }
  
  static all(promiseArr) {
    const len = promiseArr.length;
    const values = new Array(len);
    // 记录已经成功执行的promise个数
    let count = 0;
    return new Promise((resolve, reject) => {
      for (let i = 0; i < len; i++) {
        // Promise.resolve()处理,确保每一个都是promise实例
        Promise.resolve(promiseArr[i]).then(
          val => {
            values[i] = val;
            count++;
            // 如果全部执行完,返回promise的状态就可以改变了
            if (count === len) resolve(values);
          },
          err => reject(err),
        );
      }
    })
  }
  
  static race(promiseArr) {
    return new Promise((resolve, reject) => {
      promiseArr.forEach(p => {
        Promise.resolve(p).then(
          val => resolve(val),
          err => reject(err),
        )
      })
    })
  }
  
}

🔥基础应用题

1.使用Promise实现每隔1秒输出1,2,3

const arr = [1, 2, 3]
arr.reduce((p, x) => {
  return p.then(() => {
    return new Promise(resolve => {
      setTimeout(() => resolve(console.log(x)), 1000)
    })
  })
}, Promise.resolve())

使用Promise.resolve()作为初始值,然后用Promise配合着reduce不停的在promise后面叠加.then.then()里继续返回一个promise是为了下一轮继续调用then回调。

2.使用Promise实现红绿灯交替重复亮

红灯3秒亮一次,黄灯2秒亮一次,绿灯1秒亮一次;如何让三个灯不断交替重复亮灯?(用Promise实现)

// 亮灯函数
function red() {
  console.log("red");
}
function green() {
  console.log("green");
}
function yellow() {
  console.log("yellow");
}

const light = function (timer, cb) {
  return new Promise(resolve => {
    setTimeout(() => {
      cb()
      resolve()
    }, timer)
  })
}
const step = function () {
  Promise.resolve().then(() => {
    return light(3000, red)
  }).then(() => {
    return light(2000, green)
  }).then(() => {
    return light(1000, yellow)
  }).then(() => {
    return step()
  })
}

step();

3.封装一个异步加载图片的方法

function loadImg(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = function() {
      console.log("一张图片加载完成");
      resolve(img);
    };
    img.onerror = function() {
    	reject(new Error('Could not load image at' + url));
    };
    img.src = url;
  });

参考

【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理) 32个手写JS,巩固你的JS基础(面试高频)

往期JS系列文章

详解JavaScript作用域和作用域链🔗 彻底搞懂作用域、执行上下文、词法环境🔎 精选30+案例与10道题目带你彻底掌握this👊