从promise到asycn/await

4,834 阅读9分钟

一、什么是promise?

在MDN中,定义promise的只有一句话:promise对象用于表示一个异步操作的最终完成(或失败),及其结果值。

从这句话的定义我们可以抓住几个关键词:promise是对象、异步操作、最终状态及结果值。

在真正了解promise是什么前,我们不得不思考,promise的出现究竟是为了解决什么问题。

背景

javascript是单线程语言:单线程指如果有多个任务必须先排队,前面的任务执行完成后,后面的任务再执行。

同步

如果在函数返回结果的时候,调用者能够拿到预期的结果(就是函数计算的结果),那么这个函数就是同步函数。

console.log('joseydong'); // 执行后,获得了返回结果

function wait(){
    var time = (new Date()).getTime();//获取当前的unix时间戳
    while((new Date()).getTime() - time < 5000){}
    console.log('你好我的名字是');
}
wait();
console.log('josey');
console.log('说得太慢了');

在这段代码中,wait函数是一个需要耗时5秒的函数,在这5秒中,下面的console.log()函数只能等待。

这就是同步的缺点:如果一个函数是同步的,即使调用函数执行任务比较耗时,也会一直等待直到得到执行结果。

异步

如果在函数返回的时候,调用者还不能得到预期的结果,而是将来通过一定的手段得到,这就叫异步。

当执行异步函数的时候,发出调用之后会马上返回,但不会是返回预期结果;调用者不必默默等待,当得到返回结果的时候回通过回调函数主动通知调用者。

异步的实现

异步操作是会在某个时间点触发一个函数的调用。

比如AJAX就是典型的异步操作:

request.onreadystatechange = function () {
    if (request.readyState === 4) {
        if (request.status === 200) {
            return success(request.responseText);
        } else {
            return fail(request.status);
        }
    }
}

把回调函数success(request.responseText)和fail(request.status)写在一个ajax操作里,不利于维护,也不好复用。

思考一种更好的写法,比如:

var ajax = ajaxGet('url');
ajax.ifSuccess(success)
    .ifFail(fail)

这种链式写法的好处在于:先统一执行ajax逻辑,不关心如何处理返回结果,然后根据返回结果是成功还是失败,在将来的某个时候调用success函数或者fail函数。

promise

这种承诺(promise)将来会执行的对象被称为promise对象。

Promise有各种开源实现,在ES6中被统一规范,由浏览器直接支持。

二、关于promise

promise是一个对象,从这个对象中可以获取异步操作的信息。

它代表一个异步操作,有三种状态:

  • pending(进行中):初始状态
  • resolved(已完成):操作成功。又名fulfilled
  • rejected(已失败):操作失败

有了promise,就可以将异步操作以同步操作的流程表达,但它也有如下缺点:

  • 一旦创建就会立即执行,无法中途取消
  • 如果不设置回调函数,promise内部抛出的错误,不会反映到外部
  • 当处于pending状态,无法得知是刚刚开始还全是即将结束。

三、应用示例

创建promise

var promise = new Promise(
	/* executor */
	function(resolve,reject){
  		//...
	}
);

executor函数由Promise实例立即执行,传递resolved和reject函数。

在executor内部,promise有如下可能的变化:

  • resolve被调用,promise状态由pengding变为resolved,代表该promise被成功解析
  • reject被调用,promise由pengding变为rejected,代表该promise的值不能用于后续处理,被拒绝了。

1、如果在executor方法的执行过程中抛出了任何异常,那么promise立即被拒绝,相当于reject方法被调用,executor的返回值也就被忽略。

2、如果一个promise对象处在resolved或者rejected状态,那么也可以被称为settled状态。

处理Promise实例

  • then:Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。 promise.then(value => { // success 状态为resolved时调用 },error => { // failure 状态为rejected时调用 }); then方法可以接受两个回调函数作为参数: 第一个回调函数:在pending状态—>resolved状态时被调用;参数为resolve方法的返回值 第二个回调函数:在pending状态—>rejected状态时被调用;参数为reject方法的返回值;可选。
  • catch:Promise.prototype.catch方法是.then(null,rejection)的别名,用于指定发生错误时的回调函数。 promise.then(res => { // ... }).catch(error => { console.log('发生错误:',error); });
  • 扩展 Promise.prototype.then和Promise.prototype.catch方法返回promise对象,所以它可以被链式调用。但此时返回的是以函数返回值生成的新的Promise实例,不是原来的那个Promise实例。

1.Promise对象的错误具有冒泡性质,会一直向后传递,直到被捕获为止。也就是说错误一定会被catch语句捕获。

将多个Promise实例,包装成一个新的Promise实例

  • Promise.all(iterable):当所有在可迭代参数中的promises已完成时,或者当传递过程中的任何一个promise进入rejected状态,返回promise。 var promise = Promise.all([p1,p2,p3]);

        p1&&p2&&p3 都返回resolved => promise返回resolved
    
       p1||p2||p3中任意一个返回rejected=>promise状态就变成rejected,此时第一个被reject的实例返回值会传递给p的回调函数。
    

四、promise最佳实践

  • 防止then嵌套 因为then中return的还是promise,所以会执行完里面的promise再执行外面的then。此时最好将其展开,也是一样的结果,而且会更好读。

    // ------------    不好的写法   -------------
    new Promise (resolve => {
        console.log('Step 1');
        setTimeout(()=> {
            resolve('100');
        },1000)
    }).then(value => { // value => 100
        return new Promise(resolve => {
            console.log('Step 1-1');
            setTimeout(() => {
                resolve('110');
            },1000);
        })
        .then(value => { // value => 110
            console.log('Step 1-2');
            return value;
        })
        .then(value => { // value => 110
            console.log('Step 1-3')
            return value;
        })
    })
    .then(value => {
        console.log(value); // value = 110
        console.log('Step 2')
    })
    
      // ------------    好的写法    ------------
    
    new Promise((resolve) => {
    
    console.log("Step 1");
    setTimeout(() => {
      resolve("100");
    }, 1000);
    
    })
    
    .then((value) => {
      // value => 100
      return new Promise((resolve) => {
        console.log("Step 1-1");
        setTimeout(() => {
          resolve("110");
        }, 1000);
      });
    })
    .then((value) => {
      // value => 110
      console.log("Step 1-2");
      return value;
    })
    .then((value) => {
      // value => 110
      console.log("Step 1-3");
      return value;
    })
    .then((value) => {
      console.log(value); // value = 110
      console.log("Step 2");
    });
    
  • 使用.catch()捕捉错误

    通常情况下promise有如下两种处理方式:

    // ------------    不好的写法   -------------
    promise.then(function(data) {
        // success
      }, function(err) {   //仅处理promise运行时发生的错误。无法处理回调中的错误
        // error
      });
    
    // ------------    好的写法    ------------
    promise.then(res => {
        // success
    }).catch(err => {   // 处理 promise 和 前一个回调函数运行时发生的错误
        // error 
    });
    
    

因为promise抛出的错误不会传递到外层,当使用第一种写法时,成功回调的错误无法处理,因此建议使用catch方法。

  • then方法中,永远return或throw //------------ 不好的写法 ------------------ promise.then(function () { getUserInfo(userId); }).then(function () { // 在这里可能希望在这个回调中使用用户信息,但你可能会发现它根本不存在 }); 如果要使用链式then,必须返回一个promise对象。

五、async/await是什么?

async:异步,await:异步等待。

简单来说async用于声明一个function是异步的,而await用于等待一个异步方法执行完成。

语法规定await只能出现在async函数中,async函数返回的是一个Promise对象。

promise的特点是无需等待,所以在没有await的情况下执行async函数,它会立即执行,返回一个promise对象并且不会阻塞后面的语句,就和普通的promise一样。

await等待的是一个promise对象/其他值。

因为async函数返回的是一个promise对象,所以await可以用于等待一个async函数的返回值。并且,它可以等待任意表达式的结果,所以await后面可以接普通函数调用或者直接量。

function getSomething() {
    return "something";
}

async function testAsync() {
    return Promise.resolve("hello async");
}

async function test() {
    const v1 = await getSomething(); // Promise对象
    const v2 = await testAsync(); // 普通函数
    console.log(v1, v2);
}

test();

await是个运算符,用于组成表达式,await表达式的运算结果取决于它等的东西。

如果等到的不是个promise对象,那么await表达式的运算结果就等于它等到的东西。

如果等到的是个promise对象,那么await就会阻塞后面的代码,等着promise对象的resolved状态,然后得到resolve的值,作为await表达式的运算结果。

1、这就是await必须放在async函数内部的原因:async函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个promise对象中异步执行。

六、asycn/await与promise简单比较

不使用async/await

function takeLongTime() {
  return new Promise(resolve => {
    setTimeout(() => resolve('hahahha'),1000);
  });
}

takeLongTime().then(value => console.log('heihei',value))

使用async/await

function takeLongTime() {
  return new Promise(resolve => {
    setTimeout(() => resolve('hahahha'),1000);
  });
}

async function test() {
  const value = await takeLongTime();
  console.log(value);
}

test();

async/await的优势在于处理then链

Promise通过then链来解决多层回调问题,但如果需要同时处理由多个Promise组成的then链,就可以用async/await来进一步优化。

假设一个场景,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。

function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

用Promise方法来实现这几个步骤的处理:

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1580.487ms

用async/await来实现:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

1、async/await优点:代码更加简洁。

修改下上面场景的代码,后续步骤需要多个返回值:

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(m, n) {
    console.log(`step2 with ${m} and ${n}`);
    return takeLongTime(m + n);
}

function step3(k, m, n) {
    console.log(`step3 with ${k}, ${m} and ${n}`);
    return takeLongTime(k + m + n);
}

用promise处理,就会发现处理返回值会比较麻烦:

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => {
            return step2(time1, time2)
                .then(time3 => [time1, time2, time3]);
        })
        .then(times => {
            const [time1, time2, time3] = times;
            return step3(time1, time2, time3);
        })
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

用async/await处理:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time1, time2);
    const result = await step3(time1, time2, time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

// step1 with 300
// step2 with 800 = 300 + 500
// step3 with 1800 = 300 + 500 + 1000
// result is 2000
// doIt: 2907.387ms

2、async/await的优点:解决promise传递参数太麻烦的问题。

async/await使用try...catch处理rejected状态

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}

// 另一种写法

async function myFunction() {
  await somethingThatReturnsAPromise().catch(function (err){
    console.log(err);
  });
}