Skip to content

Commit 6d836af

Browse files
committed
开始写 Promise 部分
1 parent 5f3c034 commit 6d836af

File tree

5 files changed

+103
-38
lines changed

5 files changed

+103
-38
lines changed

01-2-issue.md

Lines changed: 71 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,6 @@
11
异步的问题
22
========
33

4-
无论是事件还是回调,基本原理是一致的:
5-
6-
> 把当前语句执行完;把不确定完成时间的计算交给系统;等待系统唤起回调。
7-
8-
于是带来第一个问题:**栈被破坏了,无法进行常规的 `try/catch`**
9-
10-
## 断开的栈与 `try/catch`
11-
12-
我们知道,函数执行是一个“入栈/出栈”的过程。当我们在 A 函数里调用 B 函数的时候,运行时就会先把 A 压到栈里,然后再把 B 压到栈里;B 运行结束后,出栈,然后继续执行 A;A 也运行完毕后,出栈,栈已清空,这次运行结束。
13-
14-
可是异步的回调函数(包括事件处理函数,下同)不完全如此,比如下面这段代码:
15-
16-
```javascript
17-
function callback (err, content) { // [callback]
18-
// 处理
19-
}
20-
fs.readFile('path/to/file.txt', 'utf8', callback); // [A]
21-
let foo = 123;
22-
// 继续执行其它代码
23-
```
24-
25-
A 函数执行后,并不直接调用 callback,而是继续执行其它代码,直至完成,出栈。真正调用 callback 的是运行时,启动一个新的栈,callback 作为这个栈的第一个函数。所以当函数报错的时候,我们无法获取之前栈里的信息,不容易判定是什么导致的错误。并且,如果我们在外层套一个 `try/catch`,也捕获不到错误。关于这一点,等下还会有说明。
26-
274
## 回调陷阱
285

296
这个问题其实是最直观的问题,也是大家谈的最多的问题。比如下面这段代码:
@@ -52,50 +29,107 @@ a(function (resultA) {
5229

5330
## 更严重的问题
5431

55-
面试的时候,问到回调的问题,如果候选人只能答出“回调地狱”,在我这里是不功不过不加分的。要想得到满分必须能答出更深层次的问题。
32+
面试的时候,问到回调的问题,如果候选人只能答出“回调地狱”,在我这里顶多算不功不过,不加分。要想得到满分必须能答出更深层次的问题。
5633

5734
为了说明这些问题,我们先来看一段代码。假设有这样一个需求:
5835

5936
> 遍历目录,找出最大的一个文件。
6037
6138
```javascript
39+
// 这段代码来自于 https://medium.com/@wavded/managing-node-js-callback-hell-1fe03ba8baf 我加入了一些自己的理解
6240
/**
6341
* @param dir 目标文件夹
6442
* @param callback 完成后的回调
6543
*/
6644
function findLargest(dir, callback) {
67-
fs.readdir(dir, function (err, files) {
68-
if (err) return callback(err); // [1]
69-
let count = files.length; // [2]
70-
let errored = false;
71-
let stats = [];
72-
files.forEach( file => {
73-
fs.stat(path.join(dir, file), (err, stat) => {
74-
if (errored) return; // [1]
45+
fs.readdir(dir, function (err, files) { // [1]
46+
if (err) return callback(err); // {1}
47+
let count = files.length; // {2}
48+
let errored = false; // {2}
49+
let stats = []; // {2}
50+
files.forEach( file => { // [2]
51+
fs.stat(path.join(dir, file), (err, stat) => { // [3]
52+
if (errored) return; // {1}
7553
if (err) {
7654
errored = true;
7755
return callback(err);
7856
}
79-
stats.push(stat); // [2]
57+
stats.push(stat); // [4] {2}
8058

81-
if (--count === 0) {
59+
if (--count === 0) { // [5] {2}
8260
let largest = stats
8361
.filter(function (stat) { return stat.isFile(); })
8462
.reduce(function (prev, next) {
8563
if (prev.size > next.size) return prev;
8664
return next;
8765
});
88-
callback(null, files[stats.indexOf(largest)]);
66+
callback(null, files[stats.indexOf(largest)]); // [6]
8967
}
9068
});
9169
});
9270
});
9371
}
9472

95-
findLargest('./path/to/dir', function (err, filename) {
73+
findLargest('./path/to/dir', function (err, filename) { // [7]
9674
if (err) return console.error(err);
9775
console.log('largest file was:', filename);
9876
});
9977
```
10078

101-
这里我声明了一个函数 `findLargest()`,用来查找某一个目录下体积最大的文件。大家先请看代码中标记 `[1]` 的地方
79+
这里我声明了一个函数 `findLargest()`,用来查找某一个目录下体积最大的文件。它的工作流程如下(参见代码中的标记“[n]”):
80+
81+
1. 使用 `fs.readdir` 读取一个目录下的所有文件
82+
2. 对其结果 `files` 进行遍历
83+
3. 使用 `fs.readFile` 读取每一个文件的属性
84+
4. 将其属性存入 `stats` 目录
85+
5. 每完成一个文件,就将计数器减一,直至为0,再开始查找体积最大的文件
86+
6. 通过回调传出结果
87+
7. 调用此函数的时候,需传入目标文件夹和回掉函数;回掉函数遵守 Node.js 风格,第一个参数为可能发生的错误,第二个参数为实际结果
88+
89+
我们再来看标记为“{1}”的地方。在 Node.js 中,几乎所有异步方法的回调函数都是这样一个风格:
90+
91+
```javascript
92+
/**
93+
* @param err 可能发生的错误
94+
* @param result 正确的结果
95+
*/
96+
function (err, result) {
97+
if (err) { // 如果发生错误
98+
return callback(err);
99+
}
100+
101+
// 如果一切正常
102+
callback(null, result);
103+
}
104+
```
105+
106+
通常来说,错误处理的一般机制是“捕获” -> “处理”,即 `try/catch`,但是这里我们都没有用,而是作为参数调用回调函数,甚至要一层一层的通过回调函数传出去。为什么呢?
107+
108+
## 断开的栈与 `try/catch`
109+
110+
无论是事件还是回调,基本原理是一致的:
111+
112+
> 把当前语句执行完;把不确定完成时间的计算交给系统;等待系统唤起回调。
113+
114+
于是**栈被破坏了,无法进行常规的 `try/catch`**
115+
116+
我们知道,函数执行是一个“入栈/出栈”的过程。当我们在 A 函数里调用 B 函数的时候,运行时就会先把 A 压到栈里,然后再把 B 压到栈里;B 运行结束后,出栈,然后继续执行 A;A 也运行完毕后,出栈,栈已清空,这次运行结束。
117+
118+
可是异步的回调函数(包括事件处理函数,下同)不完全如此,比如上上面的代码,无论是 `fs.readdir` 还是 `fs.readFile` 它都不会直接调用回调函数,而是继续执行其它代码,直至完成,出栈。真正调用回到函数的是运行时,并且是启用一个新栈,作为栈的第一个函数调用。所以当函数报错的时候,我们无法获取之前栈里的信息,不容易判定是什么导致的错误。并且,如果我们在外层套一个 `try/catch`,也捕获不到错误。
119+
120+
## 迫不得已使用外层变量
121+
122+
我们再来看代码中标记为“{2}”的地方。我在这里声明了3个变量,`count` 用来记录待处理文件的数量;`errored` 用来记录有没有发生错误;`stats` 用来记录文件状态。
123+
124+
这3个变量会在 `fs.stat()` 的回调函数中使用。因为我们没法确定这些异步操作的完成顺序,所以只能用这种方式判断是否所有文件都已读取完毕。虽然基于闭包的设计,这样做一定行得通,但是,操作外层作用域的变量,还是存在一些隐患。比如,这些变量同样也可以被其它同一作用域的函数访问并且修改,所以通常我们都建议关注点集中,哪里的变量就在哪里声明哪里使用哪里释放。
125+
126+
同样的原理,在第二个“{1}”这里,因为遍历已经执行完,触发回调的时候已经无力回天,所以只能记录错误,并且逐个中断。
127+
128+
## 总结
129+
130+
我们回来总结一下,异步回调的传统做法有四个问题:
131+
132+
1. 嵌套层次很深,难以维护
133+
2. 多个回调之间难以建立联系
134+
3. 无法正常使用 `try/catch/throw`
135+
4. 无法正常检索堆栈信息

01-3-growing.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
异步的发展
2+
========
3+
4+
最初,在浏览器环境下,大家遭遇的异步导致的问题还不是很严重。因为那会儿以事件侦听为主,大部分处理函数不需要嵌套很多层,也就是 Ajax 批量加载资源的时候可能有些头大,平时不怎么能听到抱怨声。
5+
6+
但是当 Node.js 问世之后,对异步的依赖一下子加剧了。
7+
8+
因为那个时候,后端语言无论是 PHP、Java、Python 都已经相当成熟,Node.js 想要站稳脚跟必须有独到之处。于是,异步函数带来的无阻塞高并发就成了 Node.js 的镇店之宝。
9+
10+
然而写了才知道,虽然能无阻塞高并发,但是无数层嵌套的回调函数也使得代码维护与重构变得异常困难,抱怨之声四起,大家更努力的探索解决方案。
11+
12+
最终,Promise/A+ 被摸索出来。

02-1-promise-basic.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Promise 入门
2+
========

02-promise-intro.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Promise 方案
2+
========
3+
4+
> 有没有一种方案,既能保留异步在无阻塞上的优势,又能让我们写代码写的更舒服呢?
5+
6+
社区经过长时间探索,最终总结出 Promise/A+ 方案,并且纳入 ES2015 规范,如今,大部分运行环境都已经原生支持它。
7+
8+
[![Promise 的普及率](https://meathill-lecture.github.io/promise-yes/img/caniuse.jpg)](http://caniuse.com/#search=promise)
9+
_Promise 的支持情况_
10+
11+
这套方案有以下好处:
12+
13+
1. 可以很好的解决回调嵌套问题
14+
2. 代码阅读体验很好
15+
3. 不需要新的语言元素
16+
17+
这套方案由大家共同探索得来,不同类库的实现略有不同,但都会支持 ES2015 标准。本文也以其为准,所以大家尽可以放心往后看。

SUMMARY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* [异步的问题](01-issues-of-async.md)
55
* [异步的起源](01-1-start.md)
66
* [异步的问题](01-2-issue.md)
7-
* [异步问题的发展](01-3-growing.md)
7+
* [异步的发展](01-3-growing.md)
88
* [Promise 方案](02-promise-intro.md)
99
* [Promise 入门](02-1-promise-basic.md)
1010
* [Promise 进阶](02-2-promise-advanced.md)

0 commit comments

Comments
 (0)