|
1 | 1 | 异步的问题
|
2 | 2 | ========
|
3 | 3 |
|
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 |
| - |
27 | 4 | ## 回调陷阱
|
28 | 5 |
|
29 | 6 | 这个问题其实是最直观的问题,也是大家谈的最多的问题。比如下面这段代码:
|
@@ -52,50 +29,107 @@ a(function (resultA) {
|
52 | 29 |
|
53 | 30 | ## 更严重的问题
|
54 | 31 |
|
55 |
| -面试的时候,问到回调的问题,如果候选人只能答出“回调地狱”,在我这里是不功不过不加分的。要想得到满分必须能答出更深层次的问题。 |
| 32 | +面试的时候,问到回调的问题,如果候选人只能答出“回调地狱”,在我这里顶多算不功不过,不加分。要想得到满分必须能答出更深层次的问题。 |
56 | 33 |
|
57 | 34 | 为了说明这些问题,我们先来看一段代码。假设有这样一个需求:
|
58 | 35 |
|
59 | 36 | > 遍历目录,找出最大的一个文件。
|
60 | 37 |
|
61 | 38 | ```javascript
|
| 39 | +// 这段代码来自于 https://medium.com/@wavded/managing-node-js-callback-hell-1fe03ba8baf 我加入了一些自己的理解 |
62 | 40 | /**
|
63 | 41 | * @param dir 目标文件夹
|
64 | 42 | * @param callback 完成后的回调
|
65 | 43 | */
|
66 | 44 | 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} |
75 | 53 | if (err) {
|
76 | 54 | errored = true;
|
77 | 55 | return callback(err);
|
78 | 56 | }
|
79 |
| - stats.push(stat); // [2] |
| 57 | + stats.push(stat); // [4] {2} |
80 | 58 |
|
81 |
| - if (--count === 0) { |
| 59 | + if (--count === 0) { // [5] {2} |
82 | 60 | let largest = stats
|
83 | 61 | .filter(function (stat) { return stat.isFile(); })
|
84 | 62 | .reduce(function (prev, next) {
|
85 | 63 | if (prev.size > next.size) return prev;
|
86 | 64 | return next;
|
87 | 65 | });
|
88 |
| - callback(null, files[stats.indexOf(largest)]); |
| 66 | + callback(null, files[stats.indexOf(largest)]); // [6] |
89 | 67 | }
|
90 | 68 | });
|
91 | 69 | });
|
92 | 70 | });
|
93 | 71 | }
|
94 | 72 |
|
95 |
| -findLargest('./path/to/dir', function (err, filename) { |
| 73 | +findLargest('./path/to/dir', function (err, filename) { // [7] |
96 | 74 | if (err) return console.error(err);
|
97 | 75 | console.log('largest file was:', filename);
|
98 | 76 | });
|
99 | 77 | ```
|
100 | 78 |
|
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. 无法正常检索堆栈信息 |
0 commit comments