@@ -24,12 +24,12 @@ a(function (resultA) {
24
24
25
25
嵌套层次之深令人发指。这种代码很难维护,有人称之为“回调地狱”,有人称之为“回调陷阱”,还有人称之为“回调金字塔”,其实都无所谓,带来的问题很明显:
26
26
27
- 1 . ** 难以维护。** 上面这段只是为演示写的示范代码,还算好懂;实际开发中,混杂了业务逻辑的代码更多更长,那才真的没法动 。
27
+ 1 . ** 难以维护。** 上面这段只是为演示写的示范代码,还算好懂;实际开发中,混杂了业务逻辑的代码更多更长,更难判定函数范围,再加上闭包导致的变量使用,那真的难以维护 。
28
28
2 . ** 难以复用。** 回调的顺序确定下来之后,想对其中的某些环节进行复用也很困难,牵一发而动全局,可能只有全靠手写,结果就会越搞越长。
29
29
30
30
## 更严重的问题
31
31
32
- 面试的时候,问到回调的问题,如果候选人只能答出“回调地狱”,在我这里顶多算不功不过,不加分。要想得到满分必须能答出更深层次的问题。
32
+ 面试的时候,问到回调的问题,如果候选人只能答出“回调地狱,难以维护 ”,在我这里顶多算不功不过,不加分。要想得到满分必须能答出更深层次的问题。
33
33
34
34
为了说明这些问题,我们先来看一段代码。假设有这样一个需求:
35
35
@@ -81,12 +81,14 @@ findLargest('./path/to/dir', function (err, filename) { // [7]
81
81
1 . 使用 ` fs.readdir ` 读取一个目录下的所有文件
82
82
2 . 对其结果 ` files ` 进行遍历
83
83
3 . 使用 ` fs.readFile ` 读取每一个文件的属性
84
- 4 . 将其属性存入 ` stats ` 目录
84
+ 4 . 将其属性存入 ` stats ` 数组
85
85
5 . 每完成一个文件,就将计数器减一,直至为0,再开始查找体积最大的文件
86
86
6 . 通过回调传出结果
87
- 7 . 调用此函数的时候,需传入目标文件夹和回掉函数;回掉函数遵守 Node.js 风格,第一个参数为可能发生的错误,第二个参数为实际结果
87
+ 7 . 调用此函数的时候,需传入目标文件夹和回调函数;回调函数遵守 Node.js 风格,第一个参数为可能发生的错误,第二个参数为实际结果
88
88
89
- 我们再来看标记为“{1}”的地方。在 Node.js 中,几乎所有异步方法的回调函数都是这样一个风格:
89
+ ## 断开的栈与 ` try/catch `
90
+
91
+ 我们再来看标记为“{1}”的地方。在 Node.js 中,几乎所有异步方法的回调函数都是这种风格:
90
92
91
93
``` javascript
92
94
/**
@@ -105,31 +107,35 @@ function (err, result) {
105
107
106
108
通常来说,错误处理的一般机制是“捕获” -> “处理”,即 ` try/catch ` ,但是这里我们都没有用,而是作为参数调用回调函数,甚至要一层一层的通过回调函数传出去。为什么呢?
107
109
108
- ## 断开的栈与 ` try/catch `
109
-
110
110
无论是事件还是回调,基本原理是一致的:
111
111
112
112
> 把当前语句执行完;把不确定完成时间的计算交给系统;等待系统唤起回调。
113
113
114
114
于是** 栈被破坏了,无法进行常规的 ` try/catch ` ** 。
115
115
116
- 我们知道,函数执行是一个“入栈/出栈”的过程。当我们在 A 函数里调用 B 函数的时候,运行时就会先把 A 压到栈里,然后再把 B 压到栈里;B 运行结束后,出栈,然后继续执行 A;A 也运行完毕后,出栈,栈已清空,这次运行结束。
116
+ 我们知道,函数执行是一个“入栈/出栈”的过程。当我们在 A 函数里调用 B 函数的时候,JS 引擎就会先把 A 压到栈里,然后再把 B 压到栈里;B 运行结束后,出栈,然后继续执行 A;A 也运行完毕后,出栈,栈已清空,这次运行结束。
117
117
118
- 可是异步的回调函数(包括事件处理函数,下同)不完全如此,比如上上面的代码,无论是 ` fs.readdir ` 还是 ` fs.readFile ` 它都不会直接调用回调函数,而是继续执行其它代码,直至完成,出栈。真正调用回到函数的是运行时,并且是启用一个新栈,作为栈的第一个函数调用。所以当函数报错的时候,我们无法获取之前栈里的信息,不容易判定是什么导致的错误。并且,如果我们在外层套一个 ` try/catch ` ,也捕获不到错误。
118
+ 可是异步回调函数(包括事件处理函数,下同)不完全如此,比如上面的代码,无论是 ` fs.readdir ` 还是 ` fs.readFile ` ,都不会直接调用回调函数,而是继续执行其它代码,直至完成,出栈。真正调用回到函数的是引擎,并且是启用一个新栈,压入栈成为第一个函数。所以如果回调报错,一方面,我们无法获取之前启动异步计算时栈里的信息,不容易判定什么导致了错误;另一方面,套在 ` fs.readdir ` 外面的 ` try/catch ` ,也根本捕获不到这个错误。
119
+
120
+ 结论:回调函数的栈与启动异步操作的栈断开了,无法正常使用 ` try/catch ` 。
119
121
120
122
## 迫不得已使用外层变量
121
123
122
124
我们再来看代码中标记为“{2}”的地方。我在这里声明了3个变量,` count ` 用来记录待处理文件的数量;` errored ` 用来记录有没有发生错误;` stats ` 用来记录文件状态。
123
125
124
- 这3个变量会在 ` fs.stat() ` 的回调函数中使用。因为我们没法确定这些异步操作的完成顺序,所以只能用这种方式判断是否所有文件都已读取完毕。虽然基于闭包的设计,这样做一定行得通,但是,操作外层作用域的变量,还是存在一些隐患。比如,这些变量同样也可以被其它同一作用域的函数访问并且修改,所以通常我们都建议关注点集中,哪里的变量就在哪里声明哪里使用哪里释放。
126
+ 这3个变量会在 ` fs.stat() ` 的回调函数中使用。因为我们没法确定这些异步操作的完成顺序,所以只能用这种方式判断是否所有文件都已读取完毕。虽然基于闭包的设计,这样做一定行得通,但是,操作外层作用域的变量,还是存在一些隐患。比如,这些变量同样也可以被其它同一作用域的函数访问并且修改。
127
+
128
+ 我们平时说“关注点集中”,哪里的变量就在哪里声明哪里使用哪里释放,就是为了避免这种情况。
129
+
130
+ 同样的原理,在第二个“{1}”这里,因为遍历已经执行完,触发回调的时候已经无力回天,所以只能根据外层作用域的记录,逐个判断。
125
131
126
- 同样的原理,在第二个“{1}”这里,因为遍历已经执行完,触发回调的时候已经无力回天,所以只能记录错误,并且逐个中断 。
132
+ 结论:同时执行多个异步回调时,因为没法预期它们的完成顺序,所以必须借助外层作用域的变量 。
127
133
128
- ## 总结
134
+ ## 小结
129
135
130
136
我们回来总结一下,异步回调的传统做法有四个问题:
131
137
132
138
1 . 嵌套层次很深,难以维护
133
- 2 . 多个回调之间难以建立联系
134
- 3 . 无法正常使用 ` try/catch/throw `
135
- 4 . 无法正常检索堆栈信息
139
+ 2 . 代码难以复用
140
+ 3 . 堆栈被破坏,无法正常检索,也无法正常使用 ` try/catch/throw `
141
+ 4 . 多个异步计算同时进行,无法预期完成顺序,必须借助外层作用域的变量,有误操作风险
0 commit comments