异步与Promise
什么是异步?
根据浏览器的进程模型,我们可以知道在浏览器环境下,JS代码都是运行在渲染主线程中的,这是一个单线程 的环境。在实际的应用中,JS中需要执行许多非常耗时的任务,比如等待网络通信、监听用户事件等等。由于渲染主线程还承担了诸如页面渲染等非常重要的工作,如果同步执行这些JS代码,就会给用户呈现网页卡死(每秒数十次的页面渲染被同步代码阻塞了)等现象,而阻塞的时间内却几乎什么都没做,只是等待一个非常耗时的操作给出它的响应,这无疑是不能接受的。异步正是为了解决这一问题而出现。
在执行到某个非常耗时的任务(例如任务A)时,渲染主线程直接将任务A交给其他线程去执行,自己则可以马上执行之后的同步代码。而当其他线程(例如线程T)完成了任务A之后,由于渲染主线程在将任务A提交给线程T的时候,同时指定了一个回调函数,因此线程T只需要将这个回调函数封装成一个任务,并添加到消息队列中即可。当渲染主线程持续消耗消息队列直到这个回调函数时,由于这个非常耗时的任务已经在其他线程中完成,渲染主线程可以直接通过这个回调函数拿到执行的结果。
在这整一个过程中,渲染主线程可以不用浪费时间等待任务A的结果,充分利用了渲染主线程宝贵的时间,所付出的代价不过是没能在任务完成的一瞬间就得到结果。这种设计无疑是非常精妙的。
单线程是异步产生的原因;事件循环是异步的解决方式。
什么是 Promise?
传统回调
在上一节中,我们知道了渲染主线程和执行任务的线程之间通过回调函数传递任务的执行结果。以下是传统的回调方式:
传统回调方式
function timeout(fn: () => void) {
setTimeout(fn, 1000);
}
function listenClick(dom: Element, fn: () => void) {
dom.addEventListener("click", fn);
}
function ajax(url: string, fn: () => void) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = fn;
xhr.open("GET", url, true);
xhr.send();
}
const handler = () => console.log("finished");
timeout(handler);
listenClick(document.getElementById("#test"), handler);
ajax("http://localhost", handler);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
我们可以看到,传统的回调方式将回调函数和其他数据一同传递,甚至直接通过对属性进行赋值来传递回调函数,格式千奇百怪。这是传统模式下的第一大问题:格式不统一。
第二大问题则是以下是著名的回调地狱的示例:
回调地狱
function waitInput(callback: (query: any) => void) {
setTimeout(() => {
/* 等待用户输入,并将用户输入存入userInput,完成后调用callback */
const userInput = { username: "Alice", password: "123456" };
callback(userInput);
}, 3000);
}
function sendRequest(query: any, callback: (res: any) => void) {
setTimeout(() => {
/* 进行网络通信,并将响应结果存入response,完成后调用callback */
const response = { code: 200, message: "success" };
callback(response);
}, 3000);
}
function refresh(callback: () => void) {
setTimeout(() => {
/* 请求刷新页面,完成后调用callback */
callback();
}, 3000);
}
function notify(message: string, callback: () => void) {
setTimeout(() => {
/* 显示提示信息,用户确认后调用callback */
callback();
}, 3000);
}
waitInput((query) => {
sendRequest(query, (res) => {
if (res.code === 200) {
refresh(() => {
notify("加载完成", () => {
console.log("用户已确认");
});
});
} else {
/* ... */
}
});
});
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
可以看到,仅仅只是进行几次简单的服务器通信和用户交互,最终打印时的缩进已经有了五层,如果业务代码稍微复杂一些,分支、循环和异步操作再多一些,代码将变得非常臃肿难以阅读,相同缩进的代码可能分属在相差好几层的不同回调函数中,即使是想分辨代码的运行顺序都变得十分困难。
为了解决这些问题,制定一个标准势在必行。
Promises/A+
Promises/A+ 是一个社区规范,但是由于它设计得简洁精巧,且非常有效,所以被许多第三方库广泛接受。最终在 ECMAScript 2015 中被吸纳,并且 ECMA 标准中的 Promise 保留了 Promises/A+ 定义的基本行为和特性。
Promises/A+ 规范主要有以下几点(官方文档):
- promise 是一个具有
then
方法的对象或者函数 - 一个 promise 具有三种状态:pending、fulfilled、rejected,且状态只能从 pending 转变到 fulfilled 或 rejected
- promise 需要保存执行结果(value)和出错原因(reason),并提供
then
进行访问,then
方法返回的也是一个 promise promise.then(onFulfilled, onRejected)
:onFulfilled 和 onRejected 只接受函数值,否则忽略;value 和 reason 作为第一个参数传递
总的来说,promise 的核心设计围绕then
方法进行,对带有then
方法的对象或函数的约束较小,或者说仅将其用于承载then
方法需要用到的数据。
在这种设计下,promise 具有一个特点“互操作性”,即只要满足 Promises/A+ 规范,那么这些对象或函数就可以互相操作,库 A 中实现的 promise,其then
方法能够接受库 B 中实现的 promise,在行为上不会有任何的差异,这也促进了规范在社区中的传播。
ES2015/ES6
在 ES2015 之后,JS终于有了一个官方的 Promise,从一个社区规范被吸纳成为官方标准之一,足见 promise 设计之优秀。围绕 Promise,ECMA 标准中还定义了 await、async 等关键字,终于统一了异步调用的方式。值得一提的是,await 和 async 只要是满足 Promises/A+ 规范的 promise 都能够结合使用,因为其本质上是一个使用 promise 的语法糖,这一点也证实了 promise 优秀的“互操作性”。
以下使用 Promise 解决上述的回调地狱:
解决回调地狱
function waitInput() {
return new Promise<any>((resolve) => {
/* 等待用户输入,并将用户输入存入userInput,完成后调用resolve */
const userInput = { username: "Alice", password: "123456" };
resolve(userInput);
});
}
function sendRequest(query: any) {
return new Promise<any>((resolve) => {
/* 进行网络通信,并将响应结果存入response,完成后调用resolve */
const response = { code: 200, message: "success" };
resolve(response);
});
}
function refresh() {
return new Promise<void>((resolve) => {
/* 请求刷新页面,完成后调用resolve */
resolve();
});
}
function notify(message: string) {
return new Promise<void>((resolve) => {
/* 显示提示信息,用户确认后调用resolve */
resolve();
});
}
waitInput()
.then((userInput) => sendRequest(userInput))
.then((res) => {
if (res.code === 200) return refresh();
else throw "请求失败";
})
.then(() => notify("加载完成"))
.then(() => console.log("用户已确认"))
.catch((err) => console.error(err));
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
29
30
31
32
33
34
35
36
37
38
39
可以看到,代码的结构变得非常清晰,各个逻辑之间的先后顺序非常易于理解。
尝试实现 Promise
通过自己动手实现 Promise,我们也可以发现 Promise 的一些特殊行为的原因和由其特性衍生出来的一些问题,比如为什么 Promise 无法捕获异步的异常,以及为什么 Fetch API 几乎涵盖了所有 XHR 的功能,却迟迟无法支持监控传输进度。
在本例中,runMicroTask
不应使用Promise.resolve().then(fn)
的方式将任务添加至微队列
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
/**
* Promise构造时会直接运行executor,并尝试捕获错误。<br/>
* 注意:如果executor内存在异步代码,由于异步代码执行时,
* 同步的try...catch代码已经结束,异步代码的错误将无法被捕获。<br/>
* 这也是Promise无法捕获异步错误的原因。
*/
export class MyPromise<T = unknown> {
#state: PromiseState = "pending";
#result: T | null | undefined = undefined;
// 同一个promise的then方法可能会多次调用,需要以数组形式存储
#handlers: {
resolve: Resolve<any>;
reject: Reject;
onFulfilled?: Fulfilled<any, any> | null | undefined;
onRejected?: Rejected<any> | null | undefined;
}[] = [];
/**
* 构造时直接运行executor,并尝试捕获错误
* @param executor
*/
constructor(executor: (resolve: Resolve<T>, reject: Reject) => void) {
const resolve = (data: T) => this.#changeState(FULFILLED, data);
const reject = (reason: any) => this.#changeState(REJECTED, reason);
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
/**
* then方法返回一个Promise
* @param onFulfilled
* @param onRejected
*/
then<R = T, E = never>(
onFulfilled?: Fulfilled<T, R> | null | undefined,
onRejected?: Rejected<E> | null | undefined,
) {
return new MyPromise<R>((resolve, reject) => {
this.#handlers.push({
resolve,
reject,
onFulfilled,
onRejected,
});
this.#run();
});
}
/**
* catch是then方法的别名,不提供onFulfilled
* @param onRejected
*/
catch<E = never>(onRejected?: Rejected<E>) {
return this.then<T | E, E>(null, onRejected);
}
/**
* 改变Promise的状态
* @param state 目标状态
* @param payload value或reason
* @private
*/
#changeState(state: Exclude<PromiseState, "pending">, payload: any) {
if (this.#state !== PENDING) return;
this.#state = state;
this.#result = payload;
this.#run();
}
/**
* 尝试运行then方法提供的回调
* @private
*/
#run() {
if (this.#state === PENDING) return;
while (this.#handlers.length) {
const { resolve, reject, onFulfilled, onRejected } =
this.#handlers.shift()!;
const callback = this.#state === FULFILLED ? onFulfilled : onRejected;
runMicroTask(() => {
if (typeof callback === "function") {
try {
const data = callback(this.#result);
// 如果返回的是promise,则由其决定接下来的行为
if (isPromiseLike(data)) {
data.then(resolve, reject);
} else {
resolve(data);
}
} catch (error) {
reject(error);
}
} else {
//! 没有提供回调,直接穿透
if (this.#state === FULFILLED) {
resolve(this.#result);
} else {
reject(this.#result);
}
}
});
}
}
}
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
export function isPromiseLike(o: any): o is MyPromiseLike<unknown> {
return o && typeof o.then === "function";
}
2
3
export function runMicroTask(fn: () => void) {
if (typeof process === "object" && typeof process.nextTick === "function") {
// Node环境下直接使用process.nextTick
process.nextTick(fn);
} else if (typeof Promise === "function") {
// 如果有Promise,使用Promise添加到微队列
Promise.resolve().then(fn);
} else if (typeof MutationObserver === "function") {
// 浏览器环境下尝试使用MutationObserver
const ob = new MutationObserver(() => {
fn(); // 执行回调
ob.disconnect(); // 停止监听
});
const node = document.createTextNode("");
ob.observe(node, { characterData: true });
node.data = "1";
} else {
// 没有API,无法添加至微队列,退而求其次使用setTimeout
//! 需要注意setTimeout无法将任务添加至微队列,是不得已的做法
setTimeout(fn, 0);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Resolve<T> = (data: T) => void;
type Reject = (reason: any) => void;
type Fulfilled<T = unknown, R = unknown> = (data: T) => R | MyPromiseLike<R>;
type Rejected<E = never> = (reason: any) => E | MyPromiseLike<E>;
type PromiseState = "pending" | "fulfilled" | "rejected";
interface MyPromiseLike<T> {
then: (
resolve: Resolve<T> | null | undefined,
reject: Reject | null | undefined,
) => void;
}
2
3
4
5
6
7
8
9
10
11
12