Skip to content

Commit aec02c9

Browse files
committed
修改一些细节
1 parent c8bce70 commit aec02c9

8 files changed

+68
-57
lines changed

Diff for: 01-1-start.md

+10-10
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77

88
在那个拨号上网的洪荒年代,浏览器还非常初级,与服务器进行数据交互的唯一方式就是提交表单。用户填写完成之后,交给服务器处理,如果内容合规当然好,如果不合规就麻烦了,必须打回来重填。那会儿网速还是论 Kb 的,比如我刚上网那会儿开始升级到 33.6Kb,主流还是 22.4Kb……
99

10-
所以很容易想象:当用户填完100+选项,按下提交按钮,等待几十秒甚至几分钟之后,反馈回来的信息却是:“您的用户名不能包含大写”,他会有多么的崩溃多么的想杀人。为了提升用户体验,网景公司的[布兰登·艾克](https://zh.wikipedia.org/wiki/%E5%B8%83%E8%98%AD%E7%99%BB%C2%B7%E8%89%BE%E5%85%8B)大约用10天时间,开发出 JavaScript 的原型,从此,这门注定改变世界的语言就诞生了。
10+
所以很容易想象:当用户填完100+选项,按下提交按钮,等待几十秒甚至几分钟之后,反馈回来的信息却是:“您的用户名不能包含大写字母”,他会有多么的崩溃多么的想杀人。为了提升用户体验,网景公司的[布兰登·艾克](https://zh.wikipedia.org/wiki/%E5%B8%83%E8%98%AD%E7%99%BB%C2%B7%E8%89%BE%E5%85%8B)大约用10天时间,开发出 JavaScript 的原型,从此,这门注定改变世界的语言就诞生了。
1111

12-
只是当时大家都还不知道,发明它的目的,只是为校验表单。
12+
只是当时大家都还没有认识到这一点,发明它的目的,只是为校验表单。
1313

1414
## JavaScript 中存在大量异步计算
1515

16-
同样为了提升用户体验,HTML DOM 也选择了边加载边生成边渲染的策略。再加上要等待用户操作,大量交互都以事件来驱动。于是,JavaScript 就存在大量的异步计算
16+
同样为了提升用户体验,HTML DOM 也选择了边加载边生成边渲染的策略。再加上要等待用户操作,大量交互都以事件来驱动。于是,JavaScript 里很早就存在着大量的异步计算
1717

1818
这也带来一个好处,作为一门 UI 语言,异步操作帮 JavaScript 避免了页面冻结。
1919

@@ -27,19 +27,19 @@
2727
2828
> 那一桌人明明已经吃上了,你只是想要菜单,这么小的一个动作,服务员却要你等待别人的一个大动作完成。你是不是很想抽ta?
2929
30-
这就是“同步”的问题:顺序交付的工作1234,必须按照1234的顺序完成。
30+
这就是“同步”的问题:**顺序交付的工作1234,必须按照1234的顺序完成。**
3131

32-
不过它的也有好处:逻辑非常简单。你不用担心每步操作会消耗多少时间,反正每步操作都会在上一步完成之后才进行,只管往后写就是了。
32+
不过“同步”也有“同步”的好处:逻辑非常简单。你不用担心每步操作会消耗多少时间,反正每一步操作都会在上一步完成之后才进行,只管往后写就是了。
3333

3434
### 异步的利弊
3535

3636
与之相反,异步,则是将耗时很长的 A 交付的工作交给系统之后,就去继续做 B 交付的工作。等到系统完成之后,再通过回调或者事件,继续做 A 剩下的工作。
3737

38-
从观察者的角度,看起来 AB 工作的完成顺序,和交付他们的时间顺序无关,所以叫"异步"
38+
从观察者的角度,看起来 AB 工作的完成顺序,和交付他们的时间顺序无关,所以叫“异步”
3939

40-
所以,那些需要大量计算(比如 Service Worker),或者复杂查询(比如 Ajax)的工作,JS 引擎把它们交给系统之后,就回来继续待机了,于是我们总能看到浏览器第一时间响应我们的操作,感觉非常好
40+
那些需要大量计算(比如 Service Worker),或者复杂查询(比如 Ajax)的工作,JS 引擎把它们交给系统之后,就立刻返回继续待机了,于是再进行什么操作,浏览器也能第一时间响应,这让用户的感觉非常好
4141

42-
有利必有弊,异步的缺点就是:必须通过特殊的语法才能实现,而这些语法看起来就不如同步那样清晰明确了
42+
有利必有弊,异步的缺点就是:必须通过特殊的语法才能实现,而这些语法就不如同步那样简单、清晰、明了
4343

4444
## 异步计算的实现
4545

@@ -65,12 +65,12 @@ document.getElementById('#button').onclick = function (event) {
6565

6666
### 回调
6767

68-
到了 Node.js(以及其它 Hybrid 环境),由于要和 JS 引擎进行交互,大部分操作都变成回调。比如用 `fs.readFile()` 读取文件内容:
68+
到了 Node.js(以及其它 Hybrid 环境),由于要和引擎外部的环境进行交互,大部分操作都变成回调。比如用 `fs.readFile()` 读取文件内容:
6969

7070
```javascript
7171
const fs = require('fs');
7272

73-
fs.readFile('path/to/file.txt', 'utf8', function (err, content) {
73+
fs.readFile('path/to/file.txt', 'utf8', (err, content) => {
7474
if (err) {
7575
throw err;
7676
}

Diff for: 01-2-issue.md

+21-15
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ a(function (resultA) {
2424

2525
嵌套层次之深令人发指。这种代码很难维护,有人称之为“回调地狱”,有人称之为“回调陷阱”,还有人称之为“回调金字塔”,其实都无所谓,带来的问题很明显:
2626

27-
1. **难以维护。** 上面这段只是为演示写的示范代码,还算好懂;实际开发中,混杂了业务逻辑的代码更多更长,那才真的没法动
27+
1. **难以维护。** 上面这段只是为演示写的示范代码,还算好懂;实际开发中,混杂了业务逻辑的代码更多更长,更难判定函数范围,再加上闭包导致的变量使用,那真的难以维护
2828
2. **难以复用。** 回调的顺序确定下来之后,想对其中的某些环节进行复用也很困难,牵一发而动全局,可能只有全靠手写,结果就会越搞越长。
2929

3030
## 更严重的问题
3131

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

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

@@ -81,12 +81,14 @@ findLargest('./path/to/dir', function (err, filename) { // [7]
8181
1. 使用 `fs.readdir` 读取一个目录下的所有文件
8282
2. 对其结果 `files` 进行遍历
8383
3. 使用 `fs.readFile` 读取每一个文件的属性
84-
4. 将其属性存入 `stats` 目录
84+
4. 将其属性存入 `stats` 数组
8585
5. 每完成一个文件,就将计数器减一,直至为0,再开始查找体积最大的文件
8686
6. 通过回调传出结果
87-
7. 调用此函数的时候,需传入目标文件夹和回掉函数;回掉函数遵守 Node.js 风格,第一个参数为可能发生的错误,第二个参数为实际结果
87+
7. 调用此函数的时候,需传入目标文件夹和回调函数;回调函数遵守 Node.js 风格,第一个参数为可能发生的错误,第二个参数为实际结果
8888

89-
我们再来看标记为“{1}”的地方。在 Node.js 中,几乎所有异步方法的回调函数都是这样一个风格:
89+
## 断开的栈与 `try/catch`
90+
91+
我们再来看标记为“{1}”的地方。在 Node.js 中,几乎所有异步方法的回调函数都是这种风格:
9092

9193
```javascript
9294
/**
@@ -105,31 +107,35 @@ function (err, result) {
105107

106108
通常来说,错误处理的一般机制是“捕获” -> “处理”,即 `try/catch`,但是这里我们都没有用,而是作为参数调用回调函数,甚至要一层一层的通过回调函数传出去。为什么呢?
107109

108-
## 断开的栈与 `try/catch`
109-
110110
无论是事件还是回调,基本原理是一致的:
111111

112112
> 把当前语句执行完;把不确定完成时间的计算交给系统;等待系统唤起回调。
113113
114114
于是**栈被破坏了,无法进行常规的 `try/catch`**
115115

116-
我们知道,函数执行是一个“入栈/出栈”的过程。当我们在 A 函数里调用 B 函数的时候,运行时就会先把 A 压到栈里,然后再把 B 压到栈里;B 运行结束后,出栈,然后继续执行 A;A 也运行完毕后,出栈,栈已清空,这次运行结束。
116+
我们知道,函数执行是一个“入栈/出栈”的过程。当我们在 A 函数里调用 B 函数的时候,JS 引擎就会先把 A 压到栈里,然后再把 B 压到栈里;B 运行结束后,出栈,然后继续执行 A;A 也运行完毕后,出栈,栈已清空,这次运行结束。
117117

118-
可是异步的回调函数(包括事件处理函数,下同)不完全如此,比如上上面的代码,无论是 `fs.readdir` 还是 `fs.readFile` 它都不会直接调用回调函数,而是继续执行其它代码,直至完成,出栈。真正调用回到函数的是运行时,并且是启用一个新栈,作为栈的第一个函数调用。所以当函数报错的时候,我们无法获取之前栈里的信息,不容易判定是什么导致的错误。并且,如果我们在外层套一个 `try/catch`,也捕获不到错误。
118+
可是异步回调函数(包括事件处理函数,下同)不完全如此,比如上面的代码,无论是 `fs.readdir` 还是 `fs.readFile`,都不会直接调用回调函数,而是继续执行其它代码,直至完成,出栈。真正调用回到函数的是引擎,并且是启用一个新栈,压入栈成为第一个函数。所以如果回调报错,一方面,我们无法获取之前启动异步计算时栈里的信息,不容易判定什么导致了错误;另一方面,套在 `fs.readdir` 外面的 `try/catch`,也根本捕获不到这个错误。
119+
120+
结论:回调函数的栈与启动异步操作的栈断开了,无法正常使用 `try/catch`
119121

120122
## 迫不得已使用外层变量
121123

122124
我们再来看代码中标记为“{2}”的地方。我在这里声明了3个变量,`count` 用来记录待处理文件的数量;`errored` 用来记录有没有发生错误;`stats` 用来记录文件状态。
123125

124-
这3个变量会在 `fs.stat()` 的回调函数中使用。因为我们没法确定这些异步操作的完成顺序,所以只能用这种方式判断是否所有文件都已读取完毕。虽然基于闭包的设计,这样做一定行得通,但是,操作外层作用域的变量,还是存在一些隐患。比如,这些变量同样也可以被其它同一作用域的函数访问并且修改,所以通常我们都建议关注点集中,哪里的变量就在哪里声明哪里使用哪里释放。
126+
这3个变量会在 `fs.stat()` 的回调函数中使用。因为我们没法确定这些异步操作的完成顺序,所以只能用这种方式判断是否所有文件都已读取完毕。虽然基于闭包的设计,这样做一定行得通,但是,操作外层作用域的变量,还是存在一些隐患。比如,这些变量同样也可以被其它同一作用域的函数访问并且修改。
127+
128+
我们平时说“关注点集中”,哪里的变量就在哪里声明哪里使用哪里释放,就是为了避免这种情况。
129+
130+
同样的原理,在第二个“{1}”这里,因为遍历已经执行完,触发回调的时候已经无力回天,所以只能根据外层作用域的记录,逐个判断。
125131

126-
同样的原理,在第二个“{1}”这里,因为遍历已经执行完,触发回调的时候已经无力回天,所以只能记录错误,并且逐个中断
132+
结论:同时执行多个异步回调时,因为没法预期它们的完成顺序,所以必须借助外层作用域的变量
127133

128-
## 总结
134+
## 小结
129135

130136
我们回来总结一下,异步回调的传统做法有四个问题:
131137

132138
1. 嵌套层次很深,难以维护
133-
2. 多个回调之间难以建立联系
134-
3. 无法正常使用 `try/catch/throw`
135-
4. 无法正常检索堆栈信息
139+
2. 代码难以复用
140+
3. 堆栈被破坏,无法正常检索,也无法正常使用 `try/catch/throw`
141+
4. 多个异步计算同时进行,无法预期完成顺序,必须借助外层作用域的变量,有误操作风险

Diff for: 01-3-growing.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
异步的发展
22
========
33

4-
最初,在浏览器环境下,大家遭遇的异步导致的问题还不是很严重。因为那会儿以事件侦听为主,大部分处理函数不需要嵌套很多层,也就是 Ajax 批量加载资源的时候可能有些头大,平时不怎么能听到抱怨声。
4+
最初,在浏览器环境下,大家遭遇的异步导致的问题还不是很严重。因为那会儿以事件侦听为主,大部分处理函数不需要嵌套很多层,也就是 Ajax 批量加载资源的时候可能有些头大,平时不怎么能听到这方面的抱怨。(所以大家都跑去做模组解决方案了,并没有在这方面很上心。)
55

66
但是当 Node.js 问世之后,对异步的依赖一下子加剧了。
77

8-
因为那个时候,后端语言无论是 PHP、Java、Python 都已经相当成熟,Node.js 想要站稳脚跟必须有独到之处。于是,异步函数带来的无阻塞高并发就成了 Node.js 的镇店之宝。
8+
因为那个时候,后端语言无论是 PHP、Java、Python 都已经相当成熟,Node.js 想要在服务器端站稳脚跟,必须有独到之处。于是,异步运算带来的无阻塞高并发就成了 Node.js 的镇店之宝、主打功能
99

10-
然而写了才知道,虽然能无阻塞高并发,但是无数层嵌套的回调函数也使得代码维护与重构变得异常困难,抱怨之声四起,大家更努力的探索解决方案
10+
然而写了才知道,虽然能无阻塞高并发,但是无数层嵌套的回调函数也使得代码维护与重构变得异常困难,抱怨之声四起,大家方开始更努力的探索解决方案
1111

1212
最终,Promise/A+ 被摸索出来。

Diff for: 01-issues-of-async.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33

44
之所以会出现这样那样的解决方案,我之所以写这样的文章介绍这些解决方案,肯定是异步本身有问题。
55

6-
是的,异步就是有那样让人难以割舍,又让人不易亲近的特质
6+
是的,异步就是那样让人难以割舍,又那样让人不易亲近

0 commit comments

Comments
 (0)