异步 Promise 以及 Async / Await

1,604 阅读6分钟

Promise 链式写法

Promise 的 then 链式写法本质上是一直往下传递一个新的Promise,也就是说then在下一步接收的是上一步返回的Promise。

看以下代码:

const setDelay = millisecond => {
  return new Promise((resolve, reject) => {
    if ('number' != typeof millisecond) reject(new Error('参数必须是number类型'))
    setTimeout(() => {
      resolve(`我延迟了${ millisecond }毫秒后输出的`)
    }, millisecond);
  })
}

const setSecond = second => {
  if ('number' != typeof second || 10 < second) throw new Error('参数必须是number类型,并且必须小于10')
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`我延迟了${ second }秒后输出的`)
    }, second * 1000);
  })
}

setDelay(3000)
    .then(res => {
      console.log(res)
      console.log('我进行到第一步')
      return setSecond(2)
    })
    .then(result => {
      console.log('现在是第二步')
      console.log(result)
    }) .catch(err => console.log(err))

// 我延迟了3000毫秒后输出的
// 我进行到第一步
// 现在是第二步
// 我延迟了2秒后输出的

错误处理

setDelay(2000)
  .then(result => {
    console.log(result)
    console.log('我是进行到第一步的')
    return setSecond(20)
  })
  .then(result => {
    console.log('我是进行到第二步的')
    console.log(result)
  }, err => console.log(err))
  .then(result => console.log('我还是继续执行的'))
  .catch(err => console.log(err))

// 我延迟了2000毫秒后输出的
// 我是进行到第一步的
// 我出错啦,进到这里捕获错误,但是不经过catch了
// 我还是继续执行的

可以看到进到 then 的第二个参数 reject 中去了,而且不再经过 catch 了。

那么我们把 catch 挪上去,写到 then 错误之前:

setDelay(2000)
  .then(result => {
    console.log(result)
    console.log('我是进行到第一步的')
    return setSecond(20)
  })
  .catch(err => console.log(err))
  .then(result => {
    console.log('我是进行到第二步的')
    console.log(result)
  }, err => console.log('我出错啦,进到这里捕获错误,但是不经过catch了'))
  .then(result => console.log('我还是继续执行的'))

// 我延迟了2000毫秒后输出的
// 我是进行到第一步的
// Error: 参数必须是number类型,并且必须小于10
// 我是进行到第二步的
// undefined
// 我还是继续执行的

可以看到先经过 catch 捕获,后面就没有错误了。

结论

  1. catch 写法是针对于整个链式写法的错误而捕获的,而 then 第二个参数是针对于上一个返回的 Promise 的。
  2. 两者的优先级别:写在前面的先捕获,后面就没有错误可以捕获了
  3. 两者都不是 break ,可以继续执行后续的操作而不受影响。
  4. 链式中的 catch 并不是终点,catch 完如果有 then 还会继续往下走。

如何跳出或停止 Promise 链式

  1. 直接拒绝某一链

    setDelay(2000)
      .then(result => {
        console.log(result)
        console.log('我是进行到第一步的')
        return setSecond(2)
      })
      .then(result => {
        console.log('我是进行到第二步的')
        console.log(result)
        console.log('我在这一层主动跳出循环')
        return Promise.reject({
          isNotErrorException: true,
          msg: '跳出循环的信息'
        })
      })
      .then(res => console.log('我不执行'))
      .catch(mes => {
        console.log(mes)
        console.log('我跳出了')
      })
    
    // 我延迟了2000毫秒后输出的
    // 我是进行到第一步的
    // 我是进行到第二步的
    // 我延迟了2秒后输出的
    // 我在这一层主动跳出循环
    // { isNotErrorException: true, msg: '跳出循环的信息' }
    // 我跳出了
    

    但是这样存在一个问题,就是当我们的 catch 放在中间,不是末尾,上述方法中止后 catch 后面的代码会继续执行,而我们又不想执行 catch 后面的代码,也就是链式的绝对中止

    如下:

    .catch 后加上这一段:.then(res => console.log('我不想执行,但是却执行了')) ,可以看到最后依然会执行这一句,那么该怎么办呢?

    这时候就要使用第二种方法了。

  2. 让当前层级一直 pending 下去

    setDelay(2000)
    .then((result)=>{
      console.log(result)
      console.log('我进行到第一步的');
      return setDelaySecond(1)
    })
    .then((result)=>{
      console.log(result);
      console.log('我主动跳出循环了');
      // return Promise.reject('跳出循环的信息')
      // 重点在这
      return new Promise(()=>{console.log('后续的不会执行')}) // 这里返回的一个新的Promise,没有resolve和reject,那么会一直处于pending状态,因为没返回啊,那么这种状态就一直保持着,中断了这个Promise
    })
    .then((result)=>{
      console.log('我不执行');
    })
    .catch((mes)=>{
      console.dir(mes)
      console.log('我跳出了');
    })
    .then((res)=>{
      console.log('我也不会执行')
    })
    

    这样就解决了上述,错误跳出而导致无法完全中止 Promise 链的问题。

    但是这可能会导致潜在的内存泄露

Await、Async

async 的本质

async 声明的函数的返回本质上是一个Promise 对象

只要声明了这个函数是 async ,那么内部不管你怎么处理,它的返回值肯定是个 Promise 对象。

来看下面的例子:

(async function () {
    return '我是Promise'
})()
//	返回的是Promise
//	Promise {<resolved>: "我是Promise"}

会自动解析成 Promise.resolve('我是Promise');

等同于:

(async function () {
	return Promise.resolve('我是Promise');
})()

await 的本质

await 等的是一个 Promise 的异步返回,如果等待的不是一个 Promise ,是起不到等待一会儿的作用的。

例如:

const demo = async () => {
  let res = await setTimeout(() => {
    console.log('我延迟了一秒')
  }, 1000)
  console.log('我由于上面的程序还没执行完,先不执行,等待一会儿')
  return res
}
demo ().then(res => console.log('输出', res))

// 我由于上面的程序还没执行完,先不执行“等待一会”
// 输出 1
// 我延迟了一秒

可以看到,并没有 await ,验证了上面所提到的注意事项。

实战演练

场景1

常规使用 asyncawait 时,我们通常将代码块使用 try...catch 来包裹,但是如果我们想拆分开来分别处理,不想因为一个的错误就整个process都crash掉了,那么难道我要写一堆 try...catch 么?

方法一

(async ()=>{
  const result = await setDelay(1000).catch(err=>{
      console.log(err)
  });
  console.log(result);
  const result1 = await setDelaySecond(12).catch(err=>{
      console.log(err)
  })
  console.log(result1);
  console.log(await setDelay(1000));
  console.log('完成了');
})()

可以看到,就算是有错误,也不会影响后续的操作。但是,这种方法的缺陷是非常不规整,await 后又跟着 catch

可以改进一下,封装一个提取错误的函数:

方法二

function to (promise) {
  return promise.then(data => {
    return [null, data]
  })
  .catch(err => [err])
}

(async ()=>{
   // es6的写法,返回一个数组(你可以改回es5的写法觉得不习惯的话),第一个是错误信息,第二个是then的异步返回数据,这里要注意一下重复变量声明可能导致问题(这里举例是全局,如果用let,const,请换变量名)。
  [err, result] = await to(setDelay(1000)) 
   // 如果err存在就是有错,不想继续执行就抛出错误
  if (err) throw new Error('出现错误,同时我不想执行了');
  console.log(result);
  [err, result1] = await to(setDelaySecond(12))
   // 还想执行就不要抛出错误
  if (err) console.log('出现错误,同时我想继续执行', err);
  console.log(result1);
  console.log(await setDelay(1000));
  console.log('完成了');
})()

场景2

依次分别延迟1秒输出值,一共5秒。

延迟函数如下:

const setDelay = (millisecond) => {
  return new Promise((resolve, reject)=>{
      if (typeof millisecond != 'number') reject(new Error('参数必须是number类型'));
      setTimeout(()=> {
        resolve(`我延迟了${millisecond}毫秒后输出的`)
      }, millisecond)
  })
}

错误的方法:

arr = [setDelay(1000), setDelay(1000), setDelay(1000)]
arr[0]
.then(result=>{
  console.log(result)
  return arr[1]
})
.then(result=>{
  console.log(result)
  return arr[2]
})
.then(result=>{
  console.log(result)
})

执行,我们发现这样输出是并行的。也就是说一秒钟一次性输出了3个值。

原因分析:

setDelay(1000) 这个直接添加到数组的时候,其实就已经执行了,注意你的执行语句 (1000)

所以我们可以将 Promise 预先存储在一个数组中,在调用的时候,再去执行。当然你也可以用闭包的方式存储起来,需要调用的时候再执行。

arr = [setDelay, setDelay, setDelay]
arr[0](1000)
.then(result=>{
  console.log(result)
  return arr[1](1000)
})
.then(result=>{
  console.log(result)
  return arr[2](1000)
})
.then(result=>{
  console.log(result)
})

优化后的Promise循环

arr = [setDelay, setDelay, setDelay]
var temp
temp = arr[0](1000)
for (let i = 1; i <= arr.length; i++) {
    if (i == arr.length) {
      temp.then(result=>{
        console.log('完成了');
      })
      break;
    }
    temp = temp.then((result)=>{
        console.log(result);
        return arr[i-1](1000)
    });
}

直接使用async/await

(async ()=>{
    arr = [timeout(2000), timeout(1000), timeout(1000)]
    for (var i=0; i < arr.length; i++) {
		result = await arr[i]();
    	console.log(result);
    }
})()