七、ES6系列之Promise

介绍

Promises 是用于传递异步计算结果的回调的替代方法。它让您能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来。 这样使得异步方法可以像同步方法那样返回值:异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。

状态:

  1. Promise 始终处于三种互斥状态之一:
    • 待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
    • 已兑现(fulfilled): 意味着操作成功完成。
    • 已拒绝(rejected): 意味着操作失败。。
  2. 如果“操作已经完成”(如果它被兑现或被拒绝),则 Promise 将被解决。
  3. 一个 Promise 只结算一次,然后保持不变。

更改状态:有两种操作可以更改 Promise 的状态。在您调用其中任何一个之后,进一步的调用将无效。

  1. Rejecting 意味着承诺被拒绝。
  2. Resolving 返回一个 Promise 用给定值解析的 对象。
    • 如果值是一个 promise,则返回该 promise;
    • 如果值是 thenable (即有 a “then” method),则返回的承诺将“跟随”该 thenable,采用其最终状态;
    • 返回的承诺将用该值实现

Promises 是一种模式,有助于进行一种特定的异步编程:异步返回单个结果的函数(或方法)。通常我们通过回调接收返回的结果:

1
2
3
4
5
6
function asyncFunction(name, age, callback) {
callback(`${name}今年${age}岁`);
}
asyncFunction("李磊", 18, (result) => {
console.log(result); // 李磊今年18岁
});

Promises 提供了一种更好的回调方法:现在异步函数返回一个 Promise 对象,该对象充当占位符和最终结果的容器。通过 Promise 方法注册的回调 then()会收到结果通知:

1
2
3
4
5
6
7
8
function asyncFunction(name, age) {
return new Promise((resolve, reject) => {
resolve(`${name}今年${age}岁`);
});
}
asyncFunction("李磊", 18).then((result) => {
console.log(result);
});

与回调相比,Promises 具有以下优点:

  • 无控制反转:与同步代码类似,基于 Promise 的函数返回结果,它们不会(直接)通过回调继续并控制执行。也就是说,调用者保持控制。
  • 链接更简单:then()可以返回一个 Promise 对象。因此,您可以链接 then()方法调用
    1
    2
    3
    4
    5
    6
    7
    8
    asyncFunction1(a, b)
    .then((result1) => {
    console.log(result1);
    return asyncFunction2(x, y);
    })
    .then((result2) => {
    console.log(result2);
    });
  • 组合异步调用(循环、映射等):稍微容易一些,因为您有可以使用的数据(Promise 对象)
  • 错误处理:正如我们稍后将看到的,使用 Promises 的错误处理更简单,因为没有控制反转。此外,异常和异步错误的管理方式相同。

来看一个列子:
使用 Node.js 风格的回调,异步读取文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
fs.readFile("config.json", function (error, text) {
if (error) {
console.error("Error while reading config file");
} else {
try {
const obj = JSON.parse(text);
console.log(JSON.stringify(obj, null, 4));
} catch (e) {
console.error("Invalid JSON in file");
}
}
});

使用 Promises,同样的功能是这样使用的:

1
2
3
4
5
6
7
8
9
10
11
readFilePromisified("config.json")
.then(function (text) {
// (A)
const obj = JSON.parse(text);
console.log(JSON.stringify(obj, null, 4));
})
.catch(function (error) {
// (B)
// File read error or JSON SyntaxError
console.error("An error occurred", error);
});

Promise.prototype.finally()

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
例如,在使用完资源后进行清理。这就是 Promise 方法 finally()的用途,它的工作方式与 finally 异常处理中的子句非常相似。它的回调不接收任何参数,但会收到解决或拒绝的通知。

1
2
3
4
5
6
7
8
9
10
createResource(···)
.then(function (value1) {
// Use resource
})
.then(function (value2) {
// Use resource
})
.finally(function () {
// Clean up
});

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

它的实现也很简单。

1
2
3
4
5
6
7
8
9
10
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
(value) => P.resolve(callback()).then(() => value),
(reason) =>
P.resolve(callback()).then(() => {
throw reason;
})
);
};

Promise.all()

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

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

p 的状态由 p1、p2、p3 决定,分成两种情况。

(1)只有 p1、p2、p3 的状态都变成 fulfilled,p 的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。

(2)只要 p1、p2、p3 之中有一个被 rejected,p 的状态就变成 rejected,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。

Promise.race()

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

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

Promise.resolve()

有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。
Promise.resolve(x) 工作原理如下:

  1. 对于大多数值 x,它返回一个满足以下条件的 Promise x:
    1
    Promise.resolve("abc").then((x) => console.log(x)); // abc
  2. 如果参数是一个 Promise 实例,那么 Promise.resolve 将不做任何修改、原封不动地返回这个实例:
    1
    2
    const p = new Promise(() => null);
    console.log(Promise.resolve(p) === p); // true
  3. 如果 参数是一个 thenable 对象。
    thenable 对象指的是具有 then 方法的对象,比如下面这个对象。
    1
    2
    3
    4
    5
    const fulfilledThenable = {
    then(resolve, reject) {
    resolve("hello");
    },
    };
    Promise.resolve()方法会将这个对象转为 Promise 对象,然后就立即执行 thenable 对象的 then()方法。
    1
    2
    3
    4
    5
    6
    7
    8
    const fulfilledThenable = {
    then(resolve, reject) {
    resolve("hello");
    },
    };
    const promise = Promise.resolve(fulfilledThenable);
    console.log(promise instanceof Promise); // true
    promise.then((x) => console.log(x)); // hello
    上面代码中,fulfilledThenable 对象的 then()方法执行后,对象 p1 的状态就变为 resolved,从而立即执行最后那个 then()方法指定的回调函数,输出”hello”。

这意味着您可以使用 Promise.resolve()将任何值(Promise、thenable 或其他)转换为 Promise。事实上,Promise.all()和 Promise.race()被用于将任意值的数组转换为 Promise 数组。

Promise.reject()

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为 rejected。

1
2
const myError = new Error("Problem!");
Promise.reject(myError).catch((err) => console.log(err === myError)); // true

示例

现在我们来看几个关于 Promise 的示例用法:

fs.readFile()

以下代码是内置 Node.js 函数的基于 Promise 的版本 fs.readFile():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { readFile } from "fs";

function readFilePromisified(filename) {
return new Promise(function (resolve, reject) {
readFile(filename, { encoding: "utf8" }, (error, data) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
}

// 调用

readFilePromisified(process.argv[2])
.then((text) => {
console.log(text);
})
.catch((error) => {
console.log(error);
});

XMLHttpRequest

下面是一个基于 Promise 的函数,它通过基于事件的 XMLHttpRequest API 执行 HTTP GET:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function httpGet(url) {
return new Promise(function (resolve, reject) {
const request = new XMLHttpRequest();
request.onload = function () {
if (this.status === 200) {
// Success
resolve(this.response);
} else {
// Something went wrong (404 etc.)
reject(new Error(this.statusText));
}
};
request.onerror = function () {
reject(new Error("XMLHttpRequest Error: " + this.statusText));
};
request.open("GET", url);
request.send();
});
}
// 调用:
httpGet("http://example.com/file.txt").then(
function (value) {
console.log("Contents: " + value);
},
function (reason) {
console.error("Something went wrong", reason);
}
);

延迟活动

基于 Promise 实现一个延迟执行函数 delay()。

1
2
3
4
5
6
7
8
9
function delay(ms) {
return new Promise(function (resolve, reject) {
setTimeout(resolve, ms);
});
}
// 调用:
delay(5000).then(function () {
console.log("5 seconds have passed!");
});

超时 Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function timeout(ms, promise) {
return new Promise(function (resolve, reject) {
promise.then(resolve);
setTimeout(function () {
reject(new Error("Timeout after " + ms + " ms")); // (A)
}, ms);
});
}
// 调用
timeout(5000, httpGet("http://example.com/file.txt"))
.then(function (value) {
console.log("Contents: " + value);
})
.catch(function (reason) {
console.error("Error or timeout", reason);
});

虽然超时后的 reject (A) 不会取消请求,但会阻止 resolve 去改变状态。

参考文献