Skip to content

Commit 713dca0

Browse files
committed
docs(async): add promise
1 parent 8bdee9a commit 713dca0

4 files changed

Lines changed: 368 additions & 6 deletions

File tree

chapters.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,4 @@
4242
- async/: 异步操作
4343
- async/general.md: 概述
4444
- async/timer.md: 定时器
45+
- async/promise.md: Promise 对象

docs/async/promise.md

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
# Promise 对象
2+
3+
## 概述
4+
5+
Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(proxy),充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。
6+
7+
注意,本章只是 Promise 对象的简单介绍。为了避免与后续教程的重复,更完整的介绍请看[《ES6 标准入门》](http://es6.ruanyifeng.com/)[《Promise 对象》](http://es6.ruanyifeng.com/#docs/promise)一章。
8+
9+
首先,Promise 是一个对象,也是一个构造函数。
10+
11+
```javascript
12+
function f1(resolve, reject) {
13+
// 异步代码...
14+
}
15+
16+
var p1 = new Promise(f1);
17+
```
18+
19+
上面代码中,`Promise`构造函数接受一个回调函数`f1`作为参数,`f1`里面是异步操作的代码。然后,返回的`p1`就是一个 Promise 实例。
20+
21+
Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个`then`方法,用来指定下一步的回调函数。
22+
23+
```javascript
24+
var p1 = new Promise(f1);
25+
p1.then(f2);
26+
```
27+
28+
上面代码中,`f1`的异步操作执行完成,就会执行`f2`
29+
30+
传统的写法可能需要把`f2`作为回调函数传入`f1`,比如写成`f1(f2)`,异步操作完成后,在`f1`内部调用`f2`。Promise 使得`f1``f2`变成了链式写法。不仅改善了可读性,而且对于多层嵌套的回调函数尤其方便。
31+
32+
```javascript
33+
// 传统写法
34+
step1(function (value1) {
35+
step2(value1, function(value2) {
36+
step3(value2, function(value3) {
37+
step4(value3, function(value4) {
38+
// ...
39+
});
40+
});
41+
});
42+
});
43+
44+
// Promise 的写法
45+
(new Promise(step1))
46+
.then(step2)
47+
.then(step3)
48+
.then(step4);
49+
```
50+
51+
从上面代码可以看到,采用 Promises 以后,程序流程变得非常清楚,十分易读。注意,为了便于理解,上面代码的`Promise`实例的生成格式,做了简化,真正的语法请参照下文。
52+
53+
总的来说,传统的回调函数写法使得代码混成一团,变得横向发展而不是向下发展。Promise 就是解决这个问题,使得异步流程可以写成同步流程。
54+
55+
Promise 原本只是社区提出的一个构想,一些函数库率先实现了这个功能。ECMAScript 6 将其写入语言标准,目前 JavaScript 原生支持 Promise 对象。
56+
57+
## Promise 对象的状态
58+
59+
Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态。
60+
61+
- 异步操作未完成(pending)
62+
- 异步操作成功(fulfilled)
63+
- 异步操作失败(rejected)
64+
65+
上面三种状态里面,`fulfilled``rejected`合在一起称为`resolved`(已定型)。
66+
67+
这三种的状态的变化途径只有两种。
68+
69+
- 从“未完成”到“成功”
70+
- 从“未完成”到“失败”
71+
72+
一旦状态发生变化,就凝固了,不会再有新的状态变化。这也是 Promise 这个名字的由来,它的英语意思是“承诺”,一旦承诺成效,就不得再改变了。这也意味着,Promise 实例的状态变化只可能发生一次。
73+
74+
因此,Promise 的最终结果只有两种。
75+
76+
- 异步操作成功,Promise 实例传回一个值(value),状态变为`fulfilled`
77+
- 异步操作失败,Promise 实例抛出一个错误(error),状态变为`rejected`
78+
79+
## Promise 构造函数
80+
81+
JavaScript 提供原生的`Promise`构造函数,用来生成 Promise 实例。
82+
83+
```javascript
84+
var promise = new Promise(function (resolve, reject) {
85+
// ...
86+
87+
if (/* 异步操作成功 */){
88+
resolve(value);
89+
} else { /* 异步操作失败 */
90+
reject(new Error());
91+
}
92+
});
93+
```
94+
95+
上面代码中,`Promise`构造函数接受一个函数作为参数,该函数的两个参数分别是`resolve``reject`。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。
96+
97+
`resolve`函数的作用是,将`Promise`实例的状态从“未完成”变为“成功”(即从`pending`变为`fulfilled`),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。`reject`函数的作用是,将`Promise`实例的状态从“未完成”变为“失败”(即从`pending`变为`rejected`),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
98+
99+
下面是一个例子。
100+
101+
```javascript
102+
function timeout(ms) {
103+
return new Promise((resolve, reject) => {
104+
setTimeout(resolve, ms, 'done');
105+
});
106+
}
107+
108+
timeout(100)
109+
```
110+
111+
上面代码中,`timeout(100)`返回一个 Promise 实例。100毫秒以后,该实例的状态会变为`fulfilled`
112+
113+
## Promise.prototype.then()
114+
115+
Promise 实例的`then`方法,用来添加回调函数。
116+
117+
`then`方法可以接受两个回调函数,第一个是异步操作成功时(变为`fulfilled`状态)时的回调函数,第二个是异步操作失败(变为`rejected`)时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。
118+
119+
```javascript
120+
var p1 = new Promise(function (resolve, reject) {
121+
resolve('成功');
122+
});
123+
p1.then(console.log, console.error);
124+
// "成功"
125+
126+
var p2 = new Promise(function (resolve, reject) {
127+
reject(new Error('失败'));
128+
});
129+
p2.then(console.log, console.error);
130+
// Error: 失败
131+
```
132+
133+
上面代码中,`p1``p2`都是Promise 实例,它们的`then`方法绑定两个回调函数:成功时的回调函数`console.log`,失败时的回调函数`console.error`(可以省略)。`p1`的状态变为成功,`p2`的状态变为失败,对应的回调函数会收到异步操作传回的值,然后在控制台输出。
134+
135+
`then`方法可以链式使用。
136+
137+
```javascript
138+
p1
139+
.then(step1)
140+
.then(step2)
141+
.then(step3)
142+
.then(
143+
console.log,
144+
console.error
145+
);
146+
```
147+
148+
上面代码中,`p1`后面有四个`then`,意味依次有四个回调函数。只要前一步的状态变为`fulfilled`,就会依次执行紧跟在后面的回调函数。
149+
150+
最后一个`then`方法,回调函数是`console.log``console.error`,用法上有一点重要的区别。`console.log`只显示`step3`的返回值,而`console.error`可以显示`p1``step1``step2``step3`之中任意一个发生的错误。举例来说,如果`step1`的状态变为`rejected`,那么`step2``step3`都不会执行了(因为它们是`resolved`的回调函数)。Promise 开始寻找,接下来第一个为`rejected`的回调函数,在上面代码中是`console.error`。这就是说,Promise 对象的报错具有传递性。
151+
152+
## then() 用法辨析
153+
154+
Promise 的用法,简单说就是一句话:使用`then`方法添加回调函数。但是,不同的写法有一些细微的差别,请看下面四种写法,它们的差别在哪里?
155+
156+
```javascript
157+
// 写法一
158+
f1().then(function () {
159+
return f2();
160+
});
161+
162+
// 写法二
163+
f1().then(function () {
164+
f2();
165+
});
166+
167+
// 写法三
168+
f1().then(f2());
169+
170+
// 写法四
171+
f1().then(f2);
172+
```
173+
174+
为了便于讲解,下面这四种写法都再用`then`方法接一个回调函数`f3`。写法一的`f3`回调函数的参数,是`f2`函数的运行结果。
175+
176+
```javascript
177+
f1().then(function () {
178+
return f2();
179+
}).then(f3);
180+
```
181+
182+
写法二的`f3`回调函数的参数是`undefined`
183+
184+
```javascript
185+
f1().then(function () {
186+
f2();
187+
return;
188+
}).then(f3);
189+
```
190+
191+
写法三的`f3`回调函数的参数,是`f2`函数返回的函数的运行结果。
192+
193+
```javascript
194+
f1().then(f2())
195+
.then(f3);
196+
```
197+
198+
写法四与写法一只有一个差别,那就是`f2`会接收到`f1()`返回的结果。
199+
200+
```javascript
201+
f1().then(f2)
202+
.then(f3);
203+
```
204+
205+
## Promise 的实例
206+
207+
### 加载图片
208+
209+
我们可以把图片的加载写成一个`Promise`对象。
210+
211+
```javascript
212+
var preloadImage = function (path) {
213+
return new Promise(function (resolve, reject) {
214+
var image = new Image();
215+
image.onload = resolve;
216+
image.onerror = reject;
217+
image.src = path;
218+
});
219+
};
220+
```
221+
222+
### Ajax 操作
223+
224+
Ajax 操作是典型的异步操作,传统上往往写成下面这样。
225+
226+
```javascript
227+
function search(term, onload, onerror) {
228+
var xhr, results, url;
229+
url = 'http://example.com/search?q=' + term;
230+
231+
xhr = new XMLHttpRequest();
232+
xhr.open('GET', url, true);
233+
234+
xhr.onload = function (e) {
235+
if (this.status === 200) {
236+
results = JSON.parse(this.responseText);
237+
onload(results);
238+
}
239+
};
240+
xhr.onerror = function (e) {
241+
onerror(e);
242+
};
243+
244+
xhr.send();
245+
}
246+
247+
search('Hello World', console.log, console.error);
248+
```
249+
250+
如果使用 Promise 对象,就可以写成下面这样。
251+
252+
```javascript
253+
function search(term) {
254+
var url = 'http://example.com/search?q=' + term;
255+
var xhr = new XMLHttpRequest();
256+
var result;
257+
258+
var p = new Promise(function (resolve, reject) {
259+
xhr.open('GET', url, true);
260+
xhr.onload = function (e) {
261+
if (this.status === 200) {
262+
result = JSON.parse(this.responseText);
263+
resolve(result);
264+
}
265+
};
266+
xhr.onerror = function (e) {
267+
reject(e);
268+
};
269+
xhr.send();
270+
});
271+
272+
return p;
273+
}
274+
275+
search('Hello World').then(console.log, console.error);
276+
```
277+
278+
加载图片的例子,也可以用 Ajax 操作完成。
279+
280+
```javascript
281+
function imgLoad(url) {
282+
return new Promise(function (resolve, reject) {
283+
var request = new XMLHttpRequest();
284+
request.open('GET', url);
285+
request.responseType = 'blob';
286+
request.onload = function () {
287+
if (request.status === 200) {
288+
resolve(request.response);
289+
} else {
290+
reject(new Error('图片加载失败:' + request.statusText));
291+
}
292+
};
293+
request.onerror = function () {
294+
reject(new Error('发生网络错误'));
295+
};
296+
request.send();
297+
});
298+
}
299+
```
300+
301+
## 小结
302+
303+
Promise 的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到它们的状态都改变以后,再执行一个回调函数;再比如,为多个回调函数中抛出的错误,统一指定处理方法等等。
304+
305+
而且,Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态。这意味着,无论何时为 Promise 实例添加回调函数,该函数都能正确执行。所以,你不用担心是否错过了某个事件或信号。如果是传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。
306+
307+
Promise 的缺点是,编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆`then`,必须自己在`then`的回调函数里面理清逻辑。
308+
309+
## 微任务
310+
311+
Promise 的回调函数属于异步任务,会在同步任务之后执行。
312+
313+
```javascript
314+
new Promise(function (resolve, reject) {
315+
resolve(1);
316+
}).then(console.log);
317+
318+
console.log(2);
319+
// 2
320+
// 1
321+
```
322+
323+
上面代码会先输出2,再输出1。因为`console.log(2)`是同步任务,而`then`的回调函数属于异步任务,一定晚于同步任务执行。
324+
325+
但是,Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务。
326+
327+
```javascript
328+
setTimeout(function() {
329+
console.log(1);
330+
}, 0);
331+
332+
new Promise(function (resolve, reject) {
333+
resolve(2);
334+
}).then(console.log);
335+
336+
console.log(3);
337+
// 3
338+
// 2
339+
// 1
340+
```
341+
342+
上面代码的输出结果是`321`。这说明`then`的回调函数的执行时间,早于`setTimeout(fn, 0)`。因为`then`是本轮事件循环执行,`setTimeout(fn, 0)`在下一轮事件循环开始时执行。
343+
344+
## 参考链接
345+
346+
- Sebastian Porto, [Asynchronous JS: Callbacks, Listeners, Control Flow Libs and Promises](http://sporto.github.com/blog/2012/12/09/callbacks-listeners-promises/)
347+
- Rhys Brett-Bowen, [Promises/A+ - understanding the spec through implementation](http://modernjavascript.blogspot.com/2013/08/promisesa-understanding-by-doing.html)
348+
- Matt Podwysocki, Amanda Silver, [Asynchronous Programming in JavaScript with “Promises”](http://blogs.msdn.com/b/ie/archive/2011/09/11/asynchronous-programming-in-javascript-with-promises.aspx)
349+
- Marc Harter, [Promise A+ Implementation](https://gist.github.com//wavded/5692344)
350+
- Bryan Klimt, [What’s so great about JavaScript Promises?](http://blog.parse.com/2013/01/29/whats-so-great-about-javascript-promises/)
351+
- Jake Archibald, [JavaScript Promises There and back again](http://www.html5rocks.com/en/tutorials/es6/promises/)
352+
- Mikito Takada, [7. Control flow, Mixu's Node book](http://book.mixu.net/node/ch7.html)

docs/async/timer.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,12 @@ setInterval(function () {
257257
}, 1000);
258258

259259
sleep(3000);
260+
261+
function sleep(ms) {
262+
var start = Date.now();
263+
while ((Date.now() - start) < ms) {
264+
}
265+
}
260266
```
261267

262268
上面代码中,`setInterval`要求每隔1000毫秒,就输出一个2。但是,紧接着的`sleep`语句需要3000毫秒才能完成,那么`setInterval`就必须推迟到3000毫秒之后才开始生效。注意,生效后`setInterval`不会产生累积效应,即不会一下子输出三个2,而是只会输出一个2。

docs/stdlib/math.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -224,17 +224,20 @@ random_str(6) // "NdQKOr"
224224

225225
`Math`对象还提供一系列三角函数方法。
226226

227-
- `Math.sin()`:返回参数的正弦
228-
- `Math.cos()`:返回参数的余弦
229-
- `Math.tan()`:返回参数的正切
230-
- `Math.asin()`:返回参数的反正弦(参数为弧度值
231-
- `Math.acos()`:返回参数的反余弦(参数为弧度值
232-
- `Math.atan()`:返回参数的反正切(参数为弧度值
227+
- `Math.sin()`:返回参数的正弦(参数为弧度值)
228+
- `Math.cos()`:返回参数的余弦(参数为弧度值)
229+
- `Math.tan()`:返回参数的正切(参数为弧度值)
230+
- `Math.asin()`:返回参数的反正弦(返回值为弧度值
231+
- `Math.acos()`:返回参数的反余弦(返回值为弧度值
232+
- `Math.atan()`:返回参数的反正切(返回值为弧度值
233233

234234
```javascript
235235
Math.sin(0) // 0
236236
Math.cos(0) // 1
237237
Math.tan(0) // 0
238+
239+
Math.sin(Math.PI / 2) // 1
240+
238241
Math.asin(1) // 1.5707963267948966
239242
Math.acos(1) // 0
240243
Math.atan(1) // 0.7853981633974483

0 commit comments

Comments
 (0)