Promise、async 和 await

回调地狱
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

推荐阅读更多精彩内容