为了账号安全,请及时绑定邮箱和手机立即绑定

Promise、async 和 await

标签:
JavaScript
回调地狱
readFile("example.txt", function(err, contents) {    if (err) {        throw err; 
    }
    writeFile("example.txt", function(err) {        if (err) {            throw err; 
        }        console.log("File was written!");
    });
});
method1(function(err, result) {    if (err) {        throw err;
    }
    method2(function(err, result) {        if (err) {            throw err;
        }
        method3(function(err, result) {            if (err) {                throw err;
            }
            method4(function(err, result) {                if (err) {                    throw err;
                }
                method5(result);
            });
        });
    });
});

回调地狱嵌套多个方法调用会创建错综复杂的代码,会难以理解与调试。当想要实现更复杂的功能时,回调函数也会存在问题。

Promise 定义

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise 特点
  1. 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。如果改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

基本用法

Promise 对象是一个构造函数,用来生成 Promise 实例。

const promise = new Promise(function(resolve, reject) {  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

  • resolve 函数的作用是,将 Promise 对象的状态从未完成变为成功(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。

  • reject 函数的作用是,将 Promise 对象的状态从未完成变为失败(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数。

promise.then(function(value) {  // success}, function(error) {  // failure});

then 方法可以接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为 resolved 时调用,第二个回调函数是 Promise 对象的状态变为 rejected 时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受 Promise 对象传出的值作为参数。

function timeout(ms) {  return new Promise((resolve, reject) => {
    setTimeout(resolve('done'), ms);
  });
}

timeout(100).then((value) => {  console.log(value);
});

如果调用 resolve 函数和 reject 函数时带有参数,那么它们的参数会被传递给回调函数。 reject 函数的参数通常是 Error 对象的实例,表示抛出的错误;resolve 函数的参数除了正常的值以外,还可能是另一个 Promise 实例。

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)
})const p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000)
})

p2.then(result => console.log(result)).catch(error => console.log(error))// Error: fail

p1 是一个 Promise,3 秒之后变为 rejected。p2 的状态在 1 秒之后改变,resolve 方法返回的是 p1。由于 p2 返回的是另一个 Promise,导致 p2 自己的状态无效了,由 p1 的状态决定 p2 的状态。所以,后面的 then 语句都变成针对后者 p1。又过了 2 秒,p1 变为rejected,导致触发 catch 方法指定的回调函数。

注意,调用 resolve 或 reject 并不会终结 Promise 的参数函数的执行。

new Promise((resolve, reject) => {
  resolve(1);  console.log(2);
}).then(r => {  console.log(r);
});// 2// 1

调用 resolve(1) 以后,后面的 console.log(2) 还是会执行,并且会首先打印出来。这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。

一般来说,调用 resolve 或 reject 以后,Promise 的使命就完成了,后继操作应该放到 then 方法里面,而不应该直接写在 resolve 或 reject 的后面。所以,最好在它们前面加上return语句,这样就不会有意外。

new Promise((resolve, reject) => {  return resolve(1);  // 后面的语句不会执行
  console.log(2);
})
finally()

finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

不管 promise 最后的状态,在执行完 then 或 catch 指定的回调函数以后,都会执行 finally 方法指定的回调函数。

已决的 Promise
const p = Promise.resolve('foo')// 等价于const p = new Promise(resolve => resolve('foo'))const p = Promise.reject('出错了');//  等价于const p = new Promise((resolve, reject) => reject('出错了'))

返回一个状态已经决定的 Promise 实例。

执行器错误

如果在执行器内部抛出了错误,那么 Promise 的拒绝处理函数就会被调用。

let promise = new Promise(function(resolve, reject) {    throw new Error("Explosion!");
});
promise.catch(function(error) {    console.log(error.message);     // "Explosion!"});
串联
let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});
p1.then(function(value) {    console.log(value);
}).then(function() {    console.log("Finished");
});

串联版本的代码中对 p1.then() 的调用返回了第二个 Promise ,又在这之上调用了 then() 。仅当第一个 Promise 已被决议后,第二个 then() 的完成处理函数才会被调用。

响应多个 Promise
  • Promise.all() 方法

接收单个可迭代对象(如数组)作为参数,并返回一个 Promise 。这个可迭代对象的元素都是 Promise ,只有在它们都完成后,所返回的 Promise 才会被完成。

let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});let p2 = new Promise(function(resolve, reject) {
    resolve(43);
});let p3 = new Promise(function(resolve, reject) {
    resolve(44);
});let p4 = Promise.all([p1, p2, p3]);
p4.then(function(value) {    console.log(Array.isArray(value));  // true
    console.log(value[0]);              //  42
    console.log(value[1]);              //  43
    console.log(value[2]);              //  44});
  • Promise.race() 方法

接受一个包含需监视的 Promise 的可迭代对象,并返回一个新的 Promise ,但一旦来源 Promise 中有一个被解决,所返回的 Promise 就会立刻被解决。与等待所有 Promise 完成的 Promise.all() 方法不同,在来源 Promise 中任意一个被完成时, Promise.race() 方法所返回的 Promise 就能作出响应。

let p1 = Promise.resolve(42);let p2 = new Promise(function(resolve, reject) {
    resolve(43);
});let p3 = new Promise(function(resolve, reject) {
    resolve(44);
});let p4 = Promise.race([p1, p2, p3]);
p4.then(function(value) {    console.log(value);     // 42});
Promise 缺点
  1. 无法取消Promise,一旦新建它就会立即执行,无法中途取消。

  2. 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。

  3. 当处于pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

async
  • async 表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

  • async 函数返回一个 Promise 对象。

  • async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。

async function f() {  return 'hello world';
}
f().then(v => console.log(v))// "hello world"

上面代码中,函数 f 内部 return 命令返回的值,会被 then 方法回调函数接收到。

  • async 函数内部抛出错误,会导致返回的 Promise 对象变为 reject 状态。抛出的错误对象会被 catch 方法回调函数接收到。

async function f() {  throw new Error('出错了');
}

f().catch(  e => console.log(e)
)// Error: 出错了
  • async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

async function asyncTest() {    let getUrl = await fetch();    console.log(getUrl);    let downloadUrl = await download();    console.log(downloadUrl);    return 'complete';
}
asyncTest().then(console.log)function fetch() {    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve('fetchDone');
        }, 1000);
    });
}function download() {    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve('downloadDone');
        }, 2000);
    });
}

上面代码中,模拟了一个抓取和一个下载,只有这两个操作全部完成,才会执行 then 方法里面的 console.log。

await
  • 正常情况下,await 命令后面是一个 Promise 对象。如果不是,会被转成一个立即 resolve 的 Promise 对象。

async function f() {  return await 123;
}
f().then(v => console.log(v))// 123

await 命令的参数是数值123,它被转成 Promise 对象,并立即 resolve。

  • await 命令后面的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到。

async function f() {  await Promise.reject('出错了');
}
f().then(v => console.log(v)).catch(e => console.log(e))// 出错了

注意,上面代码中,await 语句前面没有 return,但是 reject 方法的参数依然传入了 catch 方法的回调函数。这里如果在 await 前面加上 return,效果是一样的。

  • 只要一个 await 语句后面的 Promise 变为 reject,那么整个 async 函数都会中断执行。

async function f() {  await Promise.reject('出错了');  await Promise.resolve('hello world'); // 不会执行}
  • 有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个 await 放在 try...catch 结构里面,这样不管这个异步操作是否成功,第二个 await 都会执行。

async function f() {  try {    await Promise.reject('出错了');
  } catch(e) {
  }  return await Promise.resolve('hello world');
}

f().then(v => console.log(v))// hello world

另一种方法是 await 后面的 Promise 对象再跟一个 catch 方法,处理前面可能出现的错误。

async function f() {  await Promise.reject('出错了')
    .catch(e => console.log(e));  return await Promise.resolve('hello world');
}

f().then(v => console.log(v))// 出错了// hello world
注意点
  • await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。

async function myFunction() {  try {    await somethingThatReturnsAPromise();
  } catch (err) {    console.log(err);
  }
}// 另一种写法async function myFunction() {  await somethingThatReturnsAPromise()
  .catch(function (err) {    console.log(err);
  });
}
  • 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

// 写法一async function test() {    let [foo, bar] = await Promise.all([getFoo(), getBar()]);
}function getFoo() {    return new Promise(function(resolve, reject) {
        setTimeout(function() {            console.log('getFoo');
            resolve()
        }, 2000);
    }) 
}function getBar() {    return new Promise(function(resolve, reject) {
        setTimeout(function() {            console.log('getBar');
            resolve()
        }, 2000);
    }) 
}

test()// 写法二async function test() {    let fooPromise = getFoo();    let barPromise = getBar();    let foo = await fooPromise;    let bar = await barPromise;
}
  • await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。

async function test() {    let promises = [function1, function2, function3]

    promises.forEach(function (fun) {        const t = await fun();        console.log(t)
    });
}function function1() {    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve('function1')
        }, 2000)
    })
}function function2() {    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve('function2')
        }, 2000)
    })
}function function3() {    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve('function3')
        }, 2000)
    })
}

test()
  • 如果将 forEach 方法的参数改成 async 函数,也有问题。

promises.forEach(async function (fun) {    const t = await fun();    console.log(t)
});

上面代码可能不会正常工作,原因是这时三个异步操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用 for 循环。

for (let i of promises) {    const t = await i()    console.log(t)
}
  • 如果确实希望多个请求并发执行,可以使用 Promise.all 方法。当三个请求都 resolved 时,下面两种写法效果相同。

async function dbFuc(db) {async function test() {    let promises = [function1(), function2(), function3()]    let results = await Promise.all(promises);    console.log(results)
}
参考链接

ECMAScript 6 入门

深入理解ES6



作者:Inlight先森
链接:https://www.jianshu.com/p/d95bbcb9a6cf


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消